iOS——MRC和ARC实现原理

Objective-C提供了两种内存管理机制MRC(Mannul Reference Counting)和ARC(Automatic Reference Counting),为Objective-C提供了内存的手动和自动管理。下面我们来探讨一下MRC和ARC的实现。

OC的内存管理方式

1.自己生成的对象,自己持有

在iOS内存管理中,有四个这样的关键字:new、alloc、copy、mutableCopy,如果自身使用这些关键字的时候来产生对象,那么创建完之后,自身也就有了对象。

// 使用了alloc分配了内存,obj指向了对象,该对象本身引用计数为1,不需要retain
id obj = [[NSObject alloc] init];
// 使用了new分配了内存,objc指向了对象,该对象本身引用计数为1,不需要retain
id obj = [NSObject new];

2.非自己生成的对象,自己也能持有

// NSMutableArray通过类方法array产生了对象(并没有使用alloc、new、copy、mutableCopt来产生对象),因此该对象不属于obj自身产生的
// 因此,需要使用retain方法让对象计数器+1,从而obj可以持有该对象(尽管该对象不是他产生的)
id obj = [NSMutableArray array];
[obj retain];    

3.不再需要自己持有的对象时释放

id obj = [[NSObject alloc] init];
//释放对象
//指向对象的指针仍然被保留在变量obj中,看似可访问,但对象一经释放绝对不可访问
[obj release];

3.无法释放非自己持有的对象

id obj = [NSMutableArray array]; 
// 释放一个不属于自己的对象
// obj没有进行retain操作而进行release操作,然后autoreleasePool也会对其进行一次release操作,导致奔溃。
[obj release];

MRC实现原理

基本思想:通过手动引用计数来进行对象的内存管理

涉及方法

  • alloc/new/copy/mutableCopy:生成对象并自己持有,引用计数+1(从0变为1)
  • retain :持有对象,使对象的引用计数加1
  • release : 释放对象,使对象的引用计数减1
  • retainCount : 获取当前对象的引用计数值
  • autorelease : 当前对象会在autoreleasePool结束的时候,调用这个对象的release操作,进行引用计数减1
  • dealloc : 在MRC中若调用dealloc,需要显示的调用[super dealloc],来释放父类的相关成员变量

autorelease

autorelease即“自动释放”,是OC的一种内存自动回收机制,可以将一些临时变量通过自动释放池来回收统一释放。自动释放池销毁的时候,池子里面所有的对象都会做一次release操作iOS——MRC和ARC实现原理_第1张图片
那么,autorelease释放与简单的release释放有什么区别呢?

调用 autorelease 方法,就会把该对象放到离自己最近的自动释放池中(栈顶的释放池,多重自动释放池嵌套是以栈的形式存取的),即:使对象的持有权转移给了自动释放池(即注册到了自动释放池中),调用方拿到了对象,但这个对象还不被调用方所持有。当自动释放池销毁时,其中的所有的对象都会调用一次release操作。
iOS——MRC和ARC实现原理_第2张图片
本质上,区别在于autorelease 方法不会改变调用者的引用计数,它只是改变了对象释放时机,不再让程序员负责释放这个对象,而是交给自动释放池去处理 。

autorelease 方法相当于把调用者注册到 autoreleasepool 中,ARC环境下不能显式地调用 autorelease 方法和显式地创建 NSAutoreleasePool 对象,但可以使用@autoreleasepool { }块代替(并不代表块中所有内容都被注册到了自动释放池中)。

对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。
iOS——MRC和ARC实现原理_第3张图片
eg:

int main(int argc, const char * argv[]) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
        id obj = [[NSObject alloc]init];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [obj autorelease];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [pool drain];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
    return 0;
}

输出:
iOS——MRC和ARC实现原理_第4张图片
自动释放池使用场景

  • 循环中创建了许多临时对象,在循环里使用自动释放池,用来减少高内存占用。
  • 开启子线程的时候要自己创建自己的释放池,否则可能会发生内存泄漏。

关于retainCount

我们在MRC中,有时可能会想要打印引用计数,但retainCount方法并不是很有用,由于对象可能会处于自动释放池中,这会导致打印的引用计数并不精准,而且其他程序库也很有可能自行保留或释放对象,这都会扰乱引用计数的具体值。

同时,retainCount也存在过大的问题。看看demo:

int main(int argc, const char * argv[]) {
    NSString *firstString = @"你好";
    NSString *secondString = [NSString stringWithFormat:@"hello"];
    NSNumber *num1 = @2;
    NSNumber *num2 = @100;
    
    NSLog(@"%lu,%lu,%lu,%lu",(long)[firstString retainCount],(long)[secondString retainCount],(long)[num1 retainCount],(long)[num2 retainCount]);
    return 0;
}

请添加图片描述
系统会尽可能把NSString实现成单例对象。如果字符串像例子上的那样,是一个编译期常量,那么就可以这样来实现了。在这种情况下,编译器会把NSString对象所表示的数据放在应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无需再创建NSString对象。

NSNumber也类似,它使用了一种叫做“标签指针”的概念来标注指定类型的数值。这种做法不使用NSNumber对象,而是把与数值有关的全部消息都放在指针值里。运行期系统会在消息派发期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的NSNumber对象一样。

对于上述所说的单例对象,其保留计数绝对不会改变。这种对象的release和retain操作都是“空操作”。

ARC

内存管理方案

iOS内存管理方案有:

  • MRC和ARC
  • Tagged Pointer:专门用来处理小对象,例如NSNumber、NSDate、小NSString等
  • NONPOINTER_ISA :非指针类型的isa,主要是用来优化64位地址。在 64 位架构下,isa 指针是占 64 比特位的,实际上只有 30 多位就已经够用了,为了提高利用率,剩余的比特位存储了内存管理的相关数据内容。
    nonpointer: 表示是否对 isa 指针开启指针优化
    • 0: 纯 isa 指针
    • 1: 不止是类对象地址, isa 中包含了类信息、对象的引用计数等
  • SideTables:散列表,在散列表中主要有两个表,分别是引用计数表、弱引用表。通过 SideTables()结构来实现的,SideTables()结构下,有很多 SideTable 的数据结构。 而 sideTable 当中包含了自旋锁,引用计数表,弱引用表。 SideTables()实际上是一个哈希表,通过对象的地址来计算该对象的引用计数在哪个 sideTable 中。

修饰符

当ARC有效时,id类型和对象类型必须附加所有权修饰符,一共有如下四种。

  • __strong
  • __weak
  • __unsafe_unretained
  • __autoreleasing

__strong修饰符

__strong修饰符是id类型和对象类型默认的所有权修饰符。

id obj  = [[NSObject alloc] init];
  
//在没有明确指定所有权修饰符时,默认为__strong
id __strong obj = [[NSObject alloc] init];

不论调用哪种方法,强引用修饰的变量会持有该对象,如果已经持有则引用计数不会增加。
__strong修饰符表示对对象的强引用。持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

__strong对象相互赋值

__strong修饰符的变量不仅只在变量作用域中,在赋值上也能够正确的管理其对象的所有者。

id __strong obj0 = [[NSObject alloc] init];//生成对象A			
id __strong obj1 = [[NSObject alloc] init];//生成对象B		
id __strong obj2 = nil;
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B)	
obj1 = nil;//obj1不再强引用对象B	
obj0 = nil;//obj0不再强引用对象B	
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃

过程如下:
iOS——MRC和ARC实现原理_第5张图片

__strong循环引用带来的内存泄漏

Test类中有一个强引用类型的成员变量obj_,设置其set方法。

@interface Test : NSObject {
    id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end

#import "Test.h"

@implementation Test
- (id)init {
    self = [super init];
    return self;
}

- (void)setObject:(id __strong)obj {
    obj_ = obj;
}
@end

声明两个对象test0和test1,他们内部也有自己的成员变量obj_。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        id test0 = [[Test alloc] init];//生成TestA
        id test1 = [[Test alloc] init];//生成TestB
        [test0 setObject:test1];
        [test1 setObject:test0];
    }
    return 0;
}

如果通过set方法给两个对象的成员变量分别赋值另一个对象所持有的TestA/TestB对象,就会造成如下结果:
test0 持有 TestA,test0.obj 持有 TestB,test1 持有 TestB,test1.obj 持有 TestA。iOS——MRC和ARC实现原理_第6张图片
由于两个对象互相强引用导致内存泄露,内存泄漏就是应当废弃的对象在超出其生命周期后继续存在。

如何解决这个问题呢?接下来看__weak修饰符。

__weak修饰符

弱引用表示并不持有对象,当所引用的对象销毁了,这个变量就自动设为nil。
可以利用__weak修饰符来解决循环引用问题。

@interface Test : NSObject {
    id __weak obj_;
}
- (void)setObject:(id __strong)obj;
@end

@implementation Test
- (id)init {
    self = [super init];
    return self;
}

- (void)setObject:(id __strong)obj {
    obj_ = obj;
}
@end

使用__weak之后,test0持有对象A,test1持有对象B,而test0的object和test1的object并不持有对象A和对象B,超出作用域后,就没有变量再持有对象A和对象B,对象A和对象B就会被释放,就解决了循环引用的问题。

__unsafe_unretained修饰符

__unsafe_unretained__weak很像,唯一区别就是,__unsafe_unretained变量引用的对象再被销毁以后,不会被自动设置为nil,仍然指向对象销毁前的内存地址。所以它的名字叫做unsafe,此时你再尝试通过变量访问这个对象的属性或方法就会crash。一旦对象释放,则会成为悬垂指针,程序崩溃,因此__unnsafe_unretained修饰符的变量一定要在赋值的对象存在的情况下使用。

__autoreleasing修饰符

ARC无效时使用autorelease,在ARC下__autoreleasing的使用:

@autoreleasepool {
	id __autoreleasing obj = [[NSObject alloc] init];
}

iOS——MRC和ARC实现原理_第7张图片
@autoreleasepool块即相当于上文的NSAutoreleasePool类生成、持有及废弃。
附有__autoreleasing修饰符相当于变量调用了autorelease方法。
以下为使用__weak修饰符的例子,虽然__weak修饰符是为了避免循环引用而使用的,但在访问附有__weak修饰符的变量时,实际上必定要访问注册到AutoreleasePool的对象。

id __weak obj_= obj;
NSLog(@"%@", [obj_ class]);
//等价于:
id __weak obj_ = obj;
id __autoreleasing tmp = obj_;
NSLog(@"%@", [tmp class]);

具体ARC规则

  • 不能使用retain/release/retainCount/autorelease

  • 不能使用NSAllocateObject/NSDeallocateObject

  • 必须遵守内存管理的方法名规则
    alloc/new/copy/mutableCopy,以上述名称开始的方法在返回对象时,必须返回给调用方所应当持有的对象。这在ARC下,依然没有变。
    ARC下追加了一条命名规则:
    init,以init开头的方法,必须是实例方法,并且返回对象,类型应为id或该类的的对象类型,抑或是超类或子类型。该方法基本上只是对alloc方法返回的对象进行初始化操作并返回。
    注:initialize方法并不包含在上述命名规则里。

  • 不要显式调用dealloc
    多数情况下在dealloc中删除已注册的代理或观察者。不用书写[super dealloc],因为ARC已经自动处理了。

  • 使用@autorelease块代替NSAutoreleasePool

  • 不能使用区域(NSZone)

  • 对象型变量不能作为C语言结构体的成员
    因为ARC把内存管理的工作分配给了编译器,所以编译器必须能够知道并管理对象的生命周期。例如C语言的自动变量(局部变量)可以使用该变量的作用域来管理。但是对于结构体成员来说,是无法实现的。

  • 显式转换id和void*

id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;

ARC的工作原理

ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,在合适的地方添加相应的引用计数操作代码retain, releaseautorelease

具体一点就是, 前端编译器会为“拥有的”每一个对象插入相应的release语句。如果对象的所有权修饰符是__strong,那么它就是被拥有的。如果在某个方法内创建了一个对象(局部变量),前端编译器会在方法末尾自动插入release语句以销毁它。而类拥有的对象(实例变量/属性)会在dealloc方法内被释放。事实上,你并不需要写dealloc方法或调用父类的dealloc方法,ARC会自动帮你完成一切。此外,由编译器生成的代码甚至会比你自己写的release语句的性能还要好,因为编辑器可以作出一些假设。在ARC中,没有类可以覆盖release方法,也没有调用它的必要。ARC会通过直接使用objc_release来优化调用过程。而对于retain也是同样的方法。ARC会调用objc_retain来取代保留消息。

ARC 是工作在编译期的一种技术方案,这样的好处是:

  1. 编译之后,ARC 与非 ARC 代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc 来关闭部分源代码的 ARC 特性。
  2. 相对于垃圾回收这类内存管理方案,ARC 不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。相反,由于 ARC 能够深度分析每一个对象的生命周期,它能够做到比人工管理引用计数更加高效。例如在一个函数中,对一个对象刚开始有一个引用计数 +1的操作,之后又紧接着有一个 -1 的操作,那么编译器就可以把这两个操作都优化掉。

Q&A

ARC在编译期和运行期做了什么?

  1. 在编译期,ARC会把互相抵消的retain、release、autorelease操作约简。
  2. ARC包含有运行期组件,可以在运行期检测到autorelease和retain这一对多余的操作。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数。

我还需要为我的对象编写 dealloc 方法吗?

有时候需要。
因为ARC不会自动处理malloc/free、Core Foundation对象的生命周期管理、文件描述符等等,所以你仍然可以通过编写dealloc方法来释放这些资源。
你不必(实际上不能)释放实例变量,但可能需要对系统类和其他未使用ARC编写的代码调用[self setDelegate:nil]
ARC下的dealloc方法中不需要且不允许调用[super dealloc],Runtime会自动处理。

ARC它将 retains/releases 调用的代码放在哪了?

尝试不要去思考ARC将retains/releases调用的代码放在哪里,而是思考应用程序算法,思考对象的strong和weak指针、所有权、以及可能产生的循环引用。

ARC 中仍然可能存在循环引用吗?

是的,ARC自动retain/release,也继承了循环引用问题。幸运的是,迁移到ARC的代码很少开始泄漏,因为属性已经声明是否retain。

你可能感兴趣的:(ios,objective-c,xcode)