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_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue代替原来将对象放入AutoreleasePool中。其中objc_autoreleaseReturnValue将返回对象储存在TLS中,然后直接返回这个object(不调用autorelease)。外部接收返回值的objc_retainAutoreleasedReturnValue发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。

但调用方不是ARC呢?通过**__builtin_return_address**得到方法的返回地址。加上偏移量能够定位到调用方在调用方法后的代码,根据代码判断调用方是否为ARC。如果是,则按照此优化处理,否则按照原来autorelease方法处理。


延伸阅读


Autorelease
https://hllovesgithub.github.io/2015/11/15/2015-11-15-Autorelease/
作者
Hu Liang
发布于
2015年11月15日
许可协议