Autorelease
它的意义
对于Apple开发而言,ARC已经没有MRC容易内存出错了。在MRC的年代,一切都需要手动写,而计算机永远不会犯错,犯错的总是人,说不定在某个时刻大脑短路了或者打了一下哈欠,忘记少加了或者无意多加了个retain或者release,或者在业务逻辑交错复杂,或者在代码量显著增加,都有可能出一点小错。虽然MRC的年代已经有Static Analyse的工具,但是也不能确保是万能的。
我在MRC年代学习Autorelease时,觉得它的意义是方便内存使用:
- (void)foo {
id objc1 = [[NSObject alloc] init];
// 逻辑1
return;
// 逻辑2
if (flag1) {
id objc2 = [[NSObject alloc] init];
// ...
return;
}
// 逻辑3
while (flag2) {
// ...
}
}
在一个方法中,根据业务逻辑很可能出现多个分支,而有的分支会提前返回(对于方法返回的出口本身就是个值得研究且有争议的话题,我个人认为出口尽量越少越好)。对于MRC而言,如果有多个分支,就要做到在该分支结束返回前,把所有经过该分支上手动生成的内存全部手动释放掉。但是,所有这些因素都会引起方法中内存管理出错:
- 业务逻辑波动可能性大导致分支结构不稳定。
- 方法可能过长导致追溯检查生成对象很麻烦。
- 多个分支出有相同对象释放会引诱代码拷贝。
良好习惯
代码也是给人看的,代码是由人写的,养成良好的编码习惯,避免问题远比解决问题更有意义。比如打开关闭文件,好的习惯是open和close一起写,然后再在中间写其他逻辑。成对写好open和close后,大脑就可以放下文件管理的问题,不用在写中间逻辑时提心吊胆地不断提醒自己不要忘记close文件(往往还就真容易忘记)。这个确保了大脑逻辑最简单最清晰的时候,最安全可靠地尽早完成该完成的任务。
对于Coder,概念越抽象越好,规则越简单越好,牵挂越稀少越好。所以,反观和文件管理一样的内存管理,Autorelease(在机制上是延迟释放内存,所以内存管理还是平衡的)就能做到,在你生成对象内存的时候,就提前相应做好释放对象内存,而不用在以后的任何地方再去操心内存释放问题。有了[[[Class alloc] init] autorelease]之后,你想怎么飞就怎么飞,不用管何时返回,不用管分支有多少,不用管业务逻辑怎么变化。
同样的良好习惯的应用,还有libextobjc中的@onExit功能(用到了GCC编译的功能,设置了方法返回后的调用),在方法开头就写好返回收尾的工作,确保了安全工作就可以任性地飞,这个库以后再介绍。
言归正传
autorelease的主要作用是,和其他语言能够返回创建的临时对象一样,返回方法创建的对象。
- MRC:方法内部栈上变量会在方法返回后被回收,而objc对象都在堆上,不存在返回后对象被释放。
- ARC:方法内部所有strong指向的对象都会在{}作用域结束时被释放。
当然,无论MRC还是ARC,autorelease都保证对象延迟释放,方法的调用者不用操心获返回对象的内存管理,放心的使用。而延迟释放的时机就在Runloop。主线程会自动创建Runloop,每有一个系统事件时,Runloop都会调用一次对应的处理方法,但是进行任何处理之前会先创建autoreleasepool,在所有所有处理结束后,会drain掉autoreleasepool里所有的对象。所以能够保证,autorelease的对象在使用期间保证有效,使用之后也安全销毁。使用autorelease一般遇到的问题是,大量延迟释放的对象需要尽早销毁而不是等到Runloop这一次处理结束,从而让内存消耗平滑,常见于大量循环内部。不过,很简单,在需要的地方内嵌autoreleasepool即可。另外,Apple很多带有block的API都默认有局部autoreleasepool。
实现原理
ARC中使用@autoreleasepool{}来使用autoreleasepool,编译器将它编译成如下:
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);
这2个方法都试对AutoreleasePoolPage(一个C++实现的类)的简单封装。
- AutoreleasePool由多个AutoreleasePoolPage组合的双向链表(parent和child)
- AutoreleasePool和线程一一对应(每个Page中thread的值都相同)
- AutoreleasePoolPage每个对象会开辟4096字节内存(虚拟内存一页的大小),低地址空间存放以上成员变量,其余全部储存注册的autorelease对象地址
- next指针指向存放下一个add进来autorelease对象地址的位置
- 当一个Page空间被占满时,再新建一个Page并设好以前关系,以后add的autorelease对象将在新page中
每当调用objc_autoreleasePoolPush时,runtime向当前的Page中add进一个哨兵对象,值为0(=nil),其方法的返回值正是哨兵对象的地址,在调用objc_autoreleasePoolPop时作为内存释放的目的位置:
释放时,将整个双向链表中从最新的Page起到哨兵对象所处的Page,所有晚于哨兵的autorelease对象都发送一次release消息,并移动next到正确位置:
嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象。
ARC
Thread Local Storage(TLS)线程局部存储,将一块内存作为线程专有存储,以key-value形式进行读写,比如在非arm架构下,使用pthread提供的方法实现:
void* pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t , const void *);
ARC对autorelease返回值有优化:
// 编译前
+ (instancetype)createSark {
return [self new];
}
// caller
Sark *sark = [Sark createSark];
// 编译后
+ (instancetype)createSark {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我们调用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相当于代替我们调用了release
编译后的代码使用objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue代替原来将对象放入AutoreleasePool中。其中objc_autoreleaseReturnValue将返回对象储存在TLS中,然后直接返回这个object(不调用autorelease)。外部接收返回值的objc_retainAutoreleasedReturnValue发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。
但调用方不是ARC呢?通过**__builtin_return_address**得到方法的返回地址。加上偏移量能够定位到调用方在调用方法后的代码,根据代码判断调用方是否为ARC。如果是,则按照此优化处理,否则按照原来autorelease方法处理。
延伸阅读
- 黑幕背后的Autorelease
- 《Objective-C高级编程 iOS与OS X多线程和内存管理》