探究自动引用计数的实现

ARC 即为 “automatic reference counting”,相比 MRR,主要区别在于是人为还是编译器插入与内存管理相关的语句。此文只会记录 ARC 的内存管理规则,以及一些如弱引用、自动释放的快速返回等特性。所以这个标题起大了(目的是为了和之前的文章标题对齐),要真探究自动引用技术的实现,大多是编译器的工作了吧?

所有权修饰符

ARC 的出现引起了对引用计数模型的理解的变化:在 ARC 的环境下,开发者们不用苦思冥想加一、减一去操作对象的引用计数(这部分交给编译器去完成),只需要知道被强引用的对象会存在,不再被强引用的对象会被释放。

声明 id 类型和对象类型时都必须加上所有权(ownership)修饰符,有四个选项:__strong, __weak, __unsafe_unretained, __autoreleasing

__strong

__strong 是默认的修饰符。将对象赋给 __strong 修饰的变量后,该变量对对象有强引用,当超出变量的作用域的时候,该变量销毁,对象的强引用不复存在:

// ARC 下的
{  
    id __strong obj = [[NSObject alloc] init];
    id __strong arr = [NSMutableArray array];
}
// 等同于
// MRR 下的
{
    id obj = [[NSObject alloc] init];
    id arr = [NSMutableArray array];
    [obj release];
}

编译器对于符合命名规则的实例化方法,能正确地判断怎么释放对象,比如上面的 arr 变量就不会被发送 -release 消息。所以光 __strong 修饰符是能完成内存管理法则中的前两条的工作,至于后面如何释放,由编译器推断好了。

__weak

前面提到被强用的对象不会被销毁,那么两个对象相互强引用那就不得了了,除非打破这个循环引用,否则它们永远都不会被释放,这个时候弱引用就派上用场了。除此之外,__weak 修饰的变量,在其所指向的强引用的对象被释放时,会自动设置为 nil

__unsafe_unretained

__unsafe_unretained 貌似是为了兼容 iOS 4 及以前的运行环境而出现的。作用与 __weak 类似,不同之处在于它所修饰的变量,不会在所指对象销毁时被置 nil

id __weak weakObj;
id __unsafe_unretained unsafeObj;
@autoreleasepool {
    id obj = [[NSObject alloc] init];
    weakObj = obj;
    unsafeObj = obj;
} 
NSLog(@"%@", weakObj);     // 打印 (null)
NSLog(@"%@", unsafeObj);   // 爆炸

__autoreleasing

对象赋给由 __autoreleasing 修饰的变量时,会被注册到自动释放池中。当然像下面这样不用显式使用 __autoreleasing 修饰,作为返回值的对象,也会被编译器注册到自动释放池中(也不一定,后面会提到另一种情况):

+(id)array {
    return [NSMutableArray new];
}

关于 __autoreleasing 还有个很有意思的是,我们在写一些向上返回 NSError 对象的方法时,编译器会将 NSError ** 解释为 NSError *__autoreleasing * 像这样:

-(void)foo {
    NSError *error = nil;
    if ([self barWithError:<#(NSError *__autoreleasing *)#>]) {
        // handle error
    }
}
    
-(BOOL)barWithError:(NSError **)error {
    BOOL inevitableError = YES;
    if (error && *error) {
        *error = [[NSError alloc] init];
    }
    return inevitableError;
}

这个也是服从内存管理规则的,毕竟这个 NSError 对象不是外部调用者使用 allow/new/copy/mutableCopy 开头的方法生成并持的。

弱引用

__weak 修饰的变量是如何“自动”地被置为 nil 的?
要回答这个问题必须了解弱引用的实现,先看一个例子探究其如何存储( MRR 下也是可以开启弱引用的):

id obj = [NSObject new];
id __weak weakObj = obj;
[obj release];
NSLog(@"%@", weakObj);

结合 NSObject.mm 和 object-weak.mm 这两个文件,通过查看汇编和符号断点调试,可以推测上面例子的实际调用过程:

extern id objc_initWeak(id *location, id newObj);
extern void objc_destroyWeak(id *location);

id obj = [NSObject new];
id weakObj;
objc_initWeak(&weakObj, obj);
[obj release];
NSLog(@"%@", objc_loadWeak(&weakObj));
objc_destroyWeak(&weakObj);

介绍上面函数之前,先看一下与弱引用表等数据结构,如下图:

weak_table_struct.png

还记得那 64 个 SideTable 小格子吗?每个 SideTable 都有这么个结构体 weak_table 作为成员。而 weak_table_t 包含 weak_entries 这个指针,指向一块包含多个条目的内存区域,每一次给弱变量赋予不同的对象,都会产生一个新的条目,而当一个对象不再存在弱引用变量时,这个条目也会被移除,这块内存是大小是不断变化的。weak_entry_t 中的 referent 正是被弱引用指向的对象,下面有个联合体,其中上下两个结构体占用的内存是相同的,它们的使用是一种“或”的关系,其作用是存放弱变量的地址,当数目不超过 WEAK_INLINE_COUNT 时,把这些地址存放到 inline_referrers 这个数组中去,否则存到 referrers 指向的内存区域中,其大小也是动态变化的。

回到函数的实现,这里不贴代码,只记录其大概的工作流程:

  • objc_init() 通过简单的赋值让 weakObjobj 指向相同的对象,然后对 obj 散列,映射到一个 SideTable 的 weak_table 后,创建一个 weak_entry_t 把对象地址和 weakObj 变量的地址存起来;
  • objc_loadWeak() 任何取得 __weak 变量的值的地方都会用到这个函数,它对 &weakObj 解引用,如果解引用的结果是 nil 或者 weakObj 指向的对象在没有相应的 weak_entry_t,返回 nil;否则返回该对象并向对象发送 -retain-autorelease 消息;
  • objc_destroyWeak() 则是移除已注册的弱引用变量。如果移除后,某个对象不再有弱引用,那么释放存在于 weak_table 中条目。

那么问题来了,哪个函数将 weakObj 置为 nil 了?

答案就在 [obj release] 这一行中,当 obj 要被释放,其调用的函授大概是这样的:

objc_object::rootDealloc()
    object_dispose(id obj)
        objc_object::clearDeallocating()
            weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 

由于先前在 objc_init() 的时候存下了弱引用的地址,在 weak_clear_no_lock() 函数中很容易通过 *referrer = nil 将其置为 nil

自动释放的快速返回

好吧这个名字是我自己起的 = =,通过 objc_autoreleaseReturnValue()objc_retainAutoreleasedReturnValue() 等函数,优化内存管理,减少注册到自动释放池的对象数量。比如说下面这一段 MRR 下的代码:

 +(instancetype)randomPerson {
    Person * p = [[Person alloc] init];
    return [p autorelease];
 }
 
 +(void)test {
    Person *p = [[Person randomPerson] retain];
    [p doSomething];
    [p release];
 }

在获得 +randomPerson 后,由于我并不持有它,生怕它在某个时刻被释放掉而 do 不了 something,所以要 retain 一下。
而这份代码在 ARC 下经过编译器改写后据说是酱紫的:

 +(instancetype)randomPerson {
    Person * p = [[Person alloc] init];
    return objc_autoreleaseReturnValue(p);
}

+(void)test {
    Person *p = objc_retainAutoreleasedReturnValue([Person randomPerson]);
    [p doSomething];
    objc_storeStrong(&p, nil); // 相当于 [p release]
}

但是如果编译器知道代码会 retain 一下 +randomPerson 返回的对象,那么就不会把这个对象放到自动释放池中以减少额外的开销。

不管是什么书还是博客都这么说,但是我在测验的时候,写下这样的代码:

__strong Person *p = [Person randomPerson];
__strong Person *k = [Person randomPerson];
[p doSomething];
[k doSomething];
_objc_autoreleasePoolPrint();

还是能看到有一个 Person 对象被注册到自动释放池中。
__strong 改成 __weak 的话就合乎情理——两个对象都被注册到自动释放池中。

关于这点想了好久都没搞清楚,所以我打算得到新的 objc-runtime 的源码之后再回到这个问题上。

你可能感兴趣的:(探究自动引用计数的实现)