ARC自动管理引用计数
ARC介绍
ARC其实也是基于引用计数,只是编译器在编译时期自动在已有代码中插入合适的内存管理代码(包括 retain、release、copy、autorelease、autoreleasepool)以及在 Runtime 做一些优化。
现在的iOS开发基本都是基于ARC的,所以开发人员大部分情况都是不需要考虑内存管理的,因为编译器已经帮你做了。为什么说是大部分呢,因为底层的Core Foundation对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。
还有就算循环引起情况就算由于互相之间强引用,引用计数永远不会减到0,所以需要自己主动断开循环引用,使引用计数能够减少。
所有权修饰符
Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC中id类型和对象类其类型必须附加所有权修饰符。
其中有以下4种所有权修饰符:
__strong
__weak
__unsafe_unretaied
__autoreleasing
所有权修饰符和属性的修饰符对应关系如下所示:
assign对应的所有权类型是__unsafe_unretained
copy对应的所有权类型是__strong
retain对应的所有权类型是__strong
strong对应的所有权类型是__strong
unsafe_unretained对应的所有权类型是__unsafe_unretained
weak对应的所有权类型是__weak
__strong
__strong表示强引用,对应定义property时用到的strong。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong修饰符是 id 类型和对象类型默认的所有权修饰符。
原理:
{id__strongobj = [[NSObjectalloc] init];}
//编译器的模拟代码idobj = objc_msgSend(NSObject,@selector(alloc));objc_msgSend(obj,@selector(init));// 出作用域的时候调用objc_release(obj);
虽然ARC有效时不能使用release方法,但由此可知编译器自动插入了release。
对象是通过除alloc、new、copy、multyCopy外方法产生的情况
{id__strongobj = [NSMutableArrayarray];}
结果与之前稍有不同:
//编译器的模拟代码idobj = objc_msgSend(NSMutableArray,@selector(array));objc_retainAutoreleasedReturnValue(obj);objc_release(obj);
objc_retainAutoreleasedReturnValue函数主要用于优化程序的运行。它是用于持有(retain)对象的函数,它持有的对象应为返回注册在autoreleasePool中对象的方法,或是函数的返回值。像该源码这样,在调用array类方法之后,由编译器插入该函数。
而这种objc_retainAutoreleasedReturnValue函数是成对存在的,与之对应的函数是objc_autoreleaseReturnValue。它用于array类方法返回对象的实现上。下面看看NSMutableArray类的array方法通过编译器进行了怎样的转换:
+ (id)array{return[[NSMutableArrayalloc] init];}
//编译器模拟代码+ (id)array{idobj = objc_msgSend(NSMutableArray,@selector(alloc)); objc_msgSend(obj,@selector(init));// 代替我们调用了autorelease方法returnobjc_autoreleaseReturnValue(obj);}
我们可以看见调用了objc_autoreleaseReturnValue函数且这个函数会返回注册到自动释放池的对象,但是,这个函数有个特点,它会查看调用方的命令执行列表,如果发现接下来会调用objc_retainAutoreleasedReturnValue则不会将返回的对象注册到autoreleasePool中而仅仅返回一个对象。达到了一种最优效果。如下图:
need-to-insert-img
__weak
__weak表示弱引用,对应定义property时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。使用__weak修饰的变量,即是使用注册到autoreleasePool中的对象。__weak最常见的一个作用就是用来避免循环循环。需要注意的是,__weak修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用__unsafe_unretained修饰符来代替。
__weak 的几个使用场景:
在 Delegate 关系中防止循环引用。
在 Block 中防止循环引用。
用来修饰指向由 Interface Builder 创建的控件。比如:@property (weak, nonatomic) IBOutlet UIButton *testButton;。
原理:
{id__weakobj = [[NSObjectalloc] init]; }
编译器转换后的代码如下:
idobj;idtmp = objc_msgSend(NSObject,@selector(alloc)); objc_msgSend(tmp,@selector(init)); objc_initweak(&obj,tmp); objc_release(tmp); objc_destroyWeak(&object);
对于__weak内存管理也借助了类似于引用计数表的散列表,它通过对象的内存地址做为key,而对应的__weak修饰符变量的地址作为value注册到weak表中,在上述代码中objc_initweak就是完成这部分操作,而objc_destroyWeak
则是销毁该对象对应的value。当指向的对象被销毁时,会通过其内存地址,去weak表中查找对应的__weak修饰符变量,将其从weak表中删除。所以,weak在修饰只是让weak表增加了记录没有引起引用计数表的变化。
对象通过objc_release释放对象内存的动作如下:
objc_release
因为引用计数为0所以执行dealloc
_objc_rootDealloc
objc_dispose
objc_destructInstance
objc_clear_deallocating
而在对象被废弃时最后调用了objc_clear_deallocating,该函数的动作如下:
从weak表中获取已废弃对象内存地址对应的所有记录
将已废弃对象内存地址对应的记录中所有以weak修饰的变量都置为nil
从weak表删除已废弃对象内存地址对应的记录
根据已废弃对象内存地址从引用计数表中找到对应记录删除
据此可以解释为什么对象被销毁时对应的weak指针变量全部都置为nil,同时,也看出来销毁weak步骤较多,如果大量使用weak的话会增加CPU的负荷。
还需要确认一点是:使用__weak修饰符的变量,即是使用注册到autoreleasePool中的对象。
{id__weakobj1 = obj;NSLog(@"obj2-%@",obj1); }
编译器转换上述代码如下:
idobj1; objc_initweak(&obj1,obj);idtmp = objc_loadWeakRetained(&obj1); objc_autorelease(tmp);NSLog(@"%@",tmp); objc_destroyWeak(&obj1);
objc_loadWeakRetained函数获取附有__weak修饰符变量所引用的对象并retain,objc_autorelease函数将对象放入autoreleasePool中,据此当我们访问weak修饰指针指向的对象时,实际上是访问注册到自动释放池的对象。因此,如果大量使用weak的话,在我们去访问weak修饰的对象时,会有大量对象注册到自动释放池,这会影响程序的性能。
解决方案:要访问weak修饰的变量时,先将其赋给一个strong变量,然后进行访问
为什么访问weak修饰的对象就会访问注册到自动释放池的对象呢?
因为weak不会引起对象的引用计数器变化,因此,该对象在运行过程中很有可能会被释放。所以,需要将对象注册到自动释放池中并在autoreleasePool销毁时释放对象占用的内存。
__unsafe_unretained
ARC 是在 iOS5 引入的,而__unsafe_unretained这个修饰符主要是为了在ARC刚发布时兼容iOS4以及版本更低的系统,因为这些版本没有弱引用机制。这个修饰符在定义property时对应的是unsafe_unretained。__unsafe_unretained修饰的指针纯粹只是指向对象,没有任何额外的操作,不会去持有对象使得对象的 retainCount +1。而在指向的对象被释放时依然原原本本地指向原来的对象地址,不会被自动置为 nil,所以成为了野指针,非常不安全。
__unsafe_unretained的应用场景:
在 ARC 环境下但是要兼容 iOS4.x 的版本,用__unsafe_unretained替代 __weak 解决强循环循环的问题。
__autoreleasing
将对象赋值给附有__autoreleasing修饰符的变量等同于MRC时调用对象的autorelease方法。
@autoeleasepool {// 如果看了上面__strong的原理,就知道实际上对象已经注册到自动释放池里面了 id__autoreleasing obj = [[NSObjectalloc] init]; }
编译器转换上述代码如下:
idpool = objc_autoreleasePoolPush();idobj = objc_msgSend(NSObject,@selector(alloc)); objc_msgSend(obj,@selector(init)); objc_autorelease(obj); objc_autoreleasePoolPop(pool);@autoreleasepool{id__autoreleasing obj = [NSMutableArrayarray]; }
编译器转换上述代码如下:
idpool = objc_autoreleasePoolPush();idobj = objc_msgSend(NSMutableArray,@selector(array)); objc_retainAutoreleasedReturnValue(obj); objc_autorelease(obj); objc_autoreleasePoolPop(pool);
上面两种方式,虽然第二种持有对象的方法从alloc方法变为了objc_retainAutoreleasedReturnValue函数,都是通过objc_autorelease,注册到autoreleasePool中。
循环引用
什么是循环引用?循环引用就是在两个对象互相之间强引用了,引用计数都加1了,我们前面说过,只有当引用计数减为0时对象才释放。但是这两个的引用计数都依赖于对方,所以也就导致了永远无法释放。
最容易产生循环引用的两种情况就是Delegate和Block。所以我们就引入了弱引用这种概念,即弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。也就是我们上面所说的所有权修饰符__weak的作用。关于原理在__weak部分也有描述,简单的描述就是每一个拥有弱引用的对象都有一张表来保存弱引用的指针地址,但是这个弱引用并不会使对象引用计数加1,所以当这个对象的引用计数变为0时,系统就通过这张表,找到所有的弱引用指针把它们都置成nil。
所以在ARC中做内存管理主要就是发现这些内存泄漏,关于内存泄漏Instrument为我们提供了 Allocations/Leaks 这样的工具用来检测。但是个人觉得还是很麻烦的,大部分时候内存泄漏并不会引起应用的崩溃或者报错之类的,所以我们也不会每次主动的去查看当前代码有没有内存泄漏之类的。
这里有一个微信读书团队开源的工具MLeaksFinder,它可以在你程序运行期间,如果有内存泄漏就会弹出提示告诉你泄漏的地方。
具体原理如下:
我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。
具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。
- (BOOL)willDealloc { __weakidweakSelf =self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf assertNotDealloc]; });returnYES;}- (void)assertNotDealloc {NSAssert(NO, @“”);}
这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若3秒后没被释放,就会中断言。
Core Foundation 对象的内存管理
底层的 Core Foundation 对象,在创建时大多以 XxxCreateWithXxx 这样的方式创建,例如:
// 创建一个 CFStringRef 对象CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, “hello world", kCFStringEncodingUTF8);
// 创建一个 CTFontRef 对象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
对于这些对象的引用计数的修改,要相应的使用 CFRetain 和 CFRelease 方法。如下所示:
// 创建一个 CTFontRef 对象CTFontReffontRef =CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize,NULL);// 引用计数加 1CFRetain(fontRef);// 引用计数减 1CFRelease(fontRef);
对于CFRetain和CFRelease两个方法,读者可以直观地认为,这与 Objective-C 对象的retain和release方法等价。
所以对于底层Core Foundation对象,我们只需要延续以前手工管理引用计数的办法即可。
除此之外,还有另外一个问题需要解决。在 ARC 下,我们有时需要将一个Core Foundation对象转换成一个Objective-C对象,这个时候我们需要告诉编译器,转换过程中的引用计数需要做如何的调整。这就引入了bridge相关的关键字,以下是这些关键字的说明:
__bridge: 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
__bridge_retained:类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
__bridge_transfer:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。
参考
链接:https://www.jianshu.com/p/c3344193ce02