美文网首页
探究自动引用计数的实现

探究自动引用计数的实现

作者: StanOz | 来源:发表于2016-10-18 14:36 被阅读77次

    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 的源码之后再回到这个问题上。🤔

    相关文章

      网友评论

          本文标题:探究自动引用计数的实现

          本文链接:https://www.haomeiwen.com/subject/aqhtuttx.html