最近在重温《Objective-C 高级编程》这本书,深深为这本薄书里蕴含的"惊人"能量所倾倒.本篇文章将总结一下ARC实现的一些细节.
ARC的概念
ARC翻译过来就是自动引用计数,相信大家都知道.苹果官方文档中说明:ARC是由由编译器进行内存管理
的;本书指出了一个观点:实际上只有编译器是无法完全胜任的,在此基础上还需要Objective-C运行时库的协助
.ARC的核心就是在对象需要释放的地方自动插入release
.
说起ARC,就逃不过讨论几个修饰符:__strong,__weak,__autoreleasing和引用计数,下面一一来进行细节说明.
__strong
先看如下一段代码:
{
// ARC中默认会在对象前添加一个修饰符__strong
id obj = [[NSObject alloc] init];
//<==>等价于
id __strong obj = [[NSObject alloc] init];
}
根据runtime特性,它的实际调用如下:
{
// 消息转发
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));
// 编译器在obj作用域结束时自动插入release
objc_release(obj);
}
当然这里是以alloc/new/copy/mutableCopy
生成的对象,这种对象会被当前的变量所持有,引用计数会加1.那如果不是用被持有的方式生成对象呢?
看下面这段代码:
{
id obj = [NSMutableArray array];
}
这种方式生成的对象不会被obj持有,通常情况下会被注册到autoreleasepool
中.但也有特殊情况,上面的代码可以转换成如下代码:
{
// 消息转发
id obj = objc_msgSend(NSMutableArray,@selector(array));
// 调用objc_retainAutoreleasedReturnValue函数
objc_retainAutoreleasedReturnValue(obj);
// 编译器在obj作用域结束时自动插入release
objc_release(obj);
}
这里介绍两个相关函数:
- objc_retainAutoreleasedReturnValue():这个函数的作用是返回注册在
autoreleasepool
当中的对象. - objc_retainAutoreleaseReturnValue():这个函数一般是和
objc_retainAutoreleasedReturnValue()
成对出现的.目的是注册对象到autoreleasepool
中.但不仅限于此
.
为何说不仅限于此
呢?原因在于,objc_retainAutoreleaseReturnValue()
函数在发现对象调用了方法或者函数之后又调用了objc_retainAutoreleasedReturnValue()
,那么就不会再把返回的对象注册到autoreleasepool
中了,而是直接把对象传递过去.
这样的好处显而易见:不用再去autoreleasepool
中取出对象,传递出去,而是越过autoreleasepool
直接传递,提升了性能.
__weak
weak
修饰符想必大家都非常熟悉,它有一个众所周知的特性:用weak修饰的对象在销毁后会被自动置为nil
.另外还补充一点:凡是用weak修饰过的对象,必定是注册到autoreleasepool中的对象
.
看下面的代码:
{
// obj默认有__strong修饰
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
}
实际过程如下:
{
// 省略obj的实现
id obj1;
// 通过objc_initWeak初始化变量
objc_initWeak(&obj1,obj);
// 通过objc_destroyWeak释放变量
objc_destroyWeak(&obj1);
}
-
objc_initWeak()
函数的作用是将obj1初始化为0,然后将obj作为参数传递到这个函数中objc_storeWeak(&obj1,obj)
-
objc_destroyWeak()
函数则将0作为参数来调用:objc_storeWeak(&obj1,0)
-
objc_storeWeak()
函数的作用是以第二个参数(obj || 0)
作为key,第一个参数(&obj1)
作为value,将第一个参数的地址注册到weak表中.当key为0,即从weak表中删除变量地址.
那么weak表中的对象是如何被释放的呢?
- 从weak表中获取废弃对象的键值记录.
- 将记录中所有包含__weak的变量地址,赋值为nil.
- 从weak表中删除该记录.
- 从引用计数表中删除对应的记录.
这就是__weak修饰的变量会在释放后自动置为nil的原因.同时,因为weak修饰之后涉及到注册到weak表等相关操作,如果大量使用weak可能会造成不必要的CPU资源浪费,所以书里指出尽量在循环引用中使用weak
.
这里不得不提到另外一个和__weak相近的属性:__unsafe_unretained
,它与weak的区别在于,释放对象后不会对其置为nil,在某些特定的场合下,需要延迟释放的时候,可以考虑用这个属性修饰.
好了,下一个问题,看如下代码:
{
id __weak obj1 = obj;
// 这里使用了obj1这个用weak修饰的变量
NSLog(@"%@",obj1);
}
在weak变量被使用的情况下,实际过程如下:
{
id obj1;
objc_initWeak(&obj1,obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@",tmp);
objc_destroyWeak(&obj1);
}
从这段实现代码中我们可以看出如下几点:
- 当我们使用weak修饰的对象时,实际过程中产生了一个
tmp
对象,因为objc_loadWeakRetained()
函数会从weak表中取出weak修饰的对象,所以tmp会对这个取出的对象进行一次强引用. - 因为上述原因,weak修饰的对象在当前变量作用域结束前都可以放心使用.
-
objc_autorelease()
会将tmp对象也注册到autoreleasepool
中.所以当大量使用weak对象的时候,注册到autoreleasepool
的对象会大量增加.解决方案就是用一个__strong修饰的临时变量来使用.
{
id __weak obj1 = obj;
id tmp = obj1;
// 后面使用tmp即可
}
延伸一下:为什么有循环引用block内用weakObject的时候最好能在block内套一层strongObject?
- 在异步线程中weakObject可能会被销毁,所以需要套一层strong.
- 如果内部有耗时的循环语句,频繁使用weakObject也会增加内存损耗.
__autoreleasing
它的主要作用就是将对象注册到autoreleasepool
中.没啥好说的.
最后补充几种在ARC环境下获取引用计数的方法,但并不一定准确:ARC的一些引用计数优化,以及多线程的中的竞态条件问题,有兴趣的可以自己去了解一下
.
(1) 使用_objc_rootRetainCount()私有函数
OBJC_EXTERN int _objc_rootRetainCount(id);
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [[NSObject alloc] init];
NSLog(@"%d",_objc_rootRetainCount(obj));
}
@end
(2) 使用KVC
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [[NSObject alloc] init];
NSLog(@"%d",[[obj valueForKey:@"retainCount"] integerValue]);
}
@end
(3) 使用CFGetRetainCount()
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [[NSObject alloc] init];
NSLog(@"%d",CFGetRetainCount((__bridge CFTypeRef)(obj)));
}
@end