探究引用计数的实现

MRR 即为 “manual retain-release”,人为地插入 retain, release 等语句进行内存管理。

内存管理基础规则

整个内存管理模型都是围绕对象拥有权(object ownership)工作的:如果某个对象一直被其它对象所拥有,那么它就会存在,反之则以。遵循以下规则以保证对对象拥有权管理的正确性:

  • 自己生成的对象,自己持有(使用 allow/new/copy/mutableCopy 开头的方法生成并持有对象);

      id obj = [[NSObject alloc] init];
    
  • 非自己生成的对象,自己也能持有(发送 -retain 消息持有对象);

      NSMutableArray *array = [NSMutableArray array];
      [array retain];
    
  • 不再需要自己持有的对象时,应该交出自己的对象所有权(发送 -release 消息释放对象所有权,或者发送 -autorelease 消息延迟释放);

      id obj = [[NSObject alloc] init];
      [obj release];
    
  • 无法释放自己不持有的对象的所有权;

换做引用计数来理解,通过 +alloc/-init 等方法生成一个对象,这对对象被你所持有,它的引用计数(retain count)是 1。对它发送 -retain 消息,引用计数加一,发送 -release 消息则减一,当其引用计数为 0 时,对象所占的内存被系统回收。

引用计数的存储与操作

下面 objc-runtime 的代码来源于 RetVal 的 Github。感谢作者的修复。

引用计数的存储

要知道引用计数是如何存储与操作,除了知道与计数相关的数据结构之外,还要知道 isa 指针的存储优化(non-pointer isa)和 tagged pointer 这两项技术,这些知识在下文中对 -retainCount 等实现的理解有帮助:

non-pointer isa

isa 指针通常用来指向对象所属的类,然而在 64 位的环境下(模拟器不支持),isa 还能存储一些额外的信息,毕竟 64 个比特仅仅存储一个类的地址确实有些浪费。那么,先瞄一下 isabits 的各个指针变量(以x86_64平台的为例)

    // 变量意义来源于:http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
    // 其意义可能已经有些改变,这里列出来仅供参考。
    struct {
        uintptr_t indexed           : 1;  // 0 表示纯粹的 isa 指针,1 表示 non-pointer isa
        uintptr_t has_assoc         : 1;  // 是否有 associated object,没有的话 dealloc 会更快
        uintptr_t has_cxx_dtor      : 1;  // 是否有 C++/ARC 的析构函数,没有的话 dealloc 会更快
        uintptr_t shiftcls          : 44; // 指向类的指针
        uintptr_t magic             : 6;  // 0x02 用于在调试时区分未初始化的垃圾数据和已经初始化的对象
        uintptr_t weakly_referenced : 1;  // 是否被 weak 变量引用过,没有的话 dealloc 会更快
        uintptr_t deallocating      : 1;  // 是否正在 deallocating
        uintptr_t has_sidetable_rc  : 1;  // 引用计数值是否太大,以至于无法存在 isa 中,需要 SideTable 辅助存储
        uintptr_t extra_rc          : 8;  /* 额外的引用计数值。对象实例化时的本身的引用计数值为 1,而该值为 0。 
                                            向该对象发送 retain 消息后,extra_rc 增加 1。当 extra_rc 太大时,则需要 SideTable 辅助计数。*/
        
         #define RC_ONE   (1ULL<<56)       // bits + RC_ONE 等于 extra_rc + 1
         #define RC_HALF  (1ULL<<7)
    };

tagged pointer

同样的,tagged pointer 也是 64 位环境下一种利用指针优化存储技术,用来存储一些小对象(实际上只是栈上的一段数据,可能算不上是一个 Objective-C 对象),减少 malloc/free 在堆上的开销。在 objc_internal.h 中能看到以下的类型支持 tagged pointer:

    OBJC_TAG_NSAtom            = 0,
    OBJC_TAG_1                 = 1,
    OBJC_TAG_NSString          = 2,
    OBJC_TAG_NSNumber          = 3,
    OBJC_TAG_NSIndexPath       = 4,
    OBJC_TAG_NSManagedObjectID = 5,
    OBJC_TAG_NSDate            = 6,
    OBJC_TAG_7                 = 7

对于一个 tagged pointer,其内存布局如下:

MSB 60 bit 3 bit 1 bit LSB
< payload tag index,即上面所列出来的类型 1 表示 tagged pointer 对象,0 表示普通对象 >

你可以写这么一段代码去验证对象是否为 tagged pointer 对象,以及检查它的类型:

    NSNumber *obj = @1;
        
    uintptr_t ptr = 0xF;
    uintptr_t result = ((uintptr_t)obj & ptr);
        
    NSLog(@"obj's pointer: %p", obj);
    NSLog(@"isTaggedPointer: %lu", result & 0x1);
    NSLog(@"TaggedPointerType: %lu", (result >> 1 & 0x7));

有人会试 NSString *obj = @"Hello!";,想看看它是不是 tagged pointer。
答案是否定的。str 指向的是 TEXT 段的一个常量指针,合理的实验方式是 NSString *obj = [NSString stringWithFormat:@"Hello!"];

SideTable

上面的讨论中,我们引出了一个 SideTable 这样的东西。当一个对象的引用计数很大时(extra_rc 超出所能表示的范围),需要它辅助记录对象的引用计数。此时实际的计数值:retainCount = 1 + extra_rc + sideTable.refcnts[obj] 中的值。在 NSObject.mm 中的它,看起来大概是这样的:

    typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

    struct SideTable {
        spinlock_t slock;   // 自旋锁,保证对 sideTable 操作的原子性
        RefcountMap refcnts; // 存储引用计数的哈希表
        weak_table_t weak_table; // weak 表,这个放到 ARC 再讨论
        ...
    }

SideTable 将自旋锁、引用计数表和一个 weak 表封装到了一起。当需要根据对象读取 SideTable 时,会从一个名为 SideTableBuf 的静态数组中找到相应的 SideTable:

    // 出于某些原因以下面这种方式分配 4096 个字节,即为 64 个 sideTable 的大小
    alignas(sizeof(StripedMap<SideTable>)) static uint8_t SideTableBuf[sizeof(StripedMap<SideTable>)];

    // StripedMap 重载了 [] 运算符,具体实现可以查看源码,这里不再赘叙
    SideTable& table = SideTables()[this];

你可以理解 SideTableBuf 有 64 个格子,每个格子里面都有个 SideTable。每个对象指针可以通过计算映射到其中的一个格子中,然后再从格子中读取 refcnts 去找到自己的额外的引用计数。

值得注意的是存储引用计数的哈希表 RefcountMap refcnts,键是将对象指针包裹了一层的 DisguisedPtr,值是对象额外的引用计数值再左移两位,所以我们读取这个值的时候要再右移两位。

引用计数的操作

上面扯完了引用计数相关的数据结构,那么接下来分析 -retainCount,-retain,-release 在 objc-runtime 源码中的实现。有两点需要注意的:

  1. objc-object.h 文件中对于这些方法背后函数的实现有两套,通过条件编译的宏 SUPPORT_NONPOINTER_ISA 区分,我第一次看的时候就搞蒙了;
  2. 这些方法上面都有 // Replaced by ObjectAlloc 这样的一行注释,应该是说这些方法被 Core Foundation 的实现给替换了,所以下面的分析可能与实际的逻辑不符。

下面的分析以 SUPPORT_NONPOINTER_ISA 为真的代码为例子。

retainCount

-retainCount 的实现最终落到下面这个函数上:

inline uintptr_t 
objc_object::rootRetainCount()
{
    assert(!UseGC);
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    
    isa_t bits = LoadExclusive(&isa.bits);      if (bits.indexed) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

在调用 objc_object::rootRetainCount 时,如果当前对象使用的是 tagged pointer,那么直接返回自身的指针值。因为考究存在于栈上的变量的引用计数几乎没有什么意义,它的生命周期由栈来管理。接着,如果对象使用了 non-pointer isa,并且没有使用 SideTable 辅助计数,那么返回对象实例化后的计数值 1 加上额外被 retain 的次数 extra_rc(objc_object::sidetable_getExtraRC_nolock 这个函数实现就不贴了,同下面的差不多)。

对于使用纯粹的 isa 指针的对象,会调到下面这个函数,从 SideTable 中获得计数表,通过 this 指针获得迭代器并访问引用计数值:

uintptr_t
objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;
    
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}

retain & release

理解上面获取引用计数的函数实现之后,对于 retain 和 release 的实现就不难理解了。但由于 id objc_object::rootRetain(bool, bool)bool objc_object::rootRelease(bool, bool) 的实现都比较长,贴在这里有凑字数的嫌疑,而且使用了很多 goto 和递归,阅读起来也不太方便。

所以下面仅对一些关键的逻辑进行分析:

  • id objc_object::rootRetain(bool, bool) 中,如果对象是 tagged pointer object,那么直接返回该对象;对于普通的对象,如果其 isa 指针不用于优化存储,那么通过 goto unindexed; 跳到 unindexed 标签所标记的代码块,对 SideTable 的计数表进行操作;否则进入 do...while() 循环里面,通过下面的代码对 bits.extra 操作:

      newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
    

一旦溢出,对象启用 SideTable 辅助计数,extra_rc 的值为最大值的一半,而将另一半拷贝到对应的 SideTable 中的计数表中。

 // 每次溢出,transcribeToSideTable 为真
 if (transcribeToSideTable) {
    sidetable_addExtraRC_nolock(RC_HALF);
}
  • bool objc_object::rootRelease(bool, bool) 中,对于 tagged pointer object 还是没有任何操作,直接返回。对于 goto unindexed; 跳转的那一块代码,调用 sidetable_release() 函数操作计数表。而在

      newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
    

之后,如果 extra_rc 出现下溢,那么要跳转到 underflow 那一块代码进行操作,从对象的辅助计数表中把原先加到里面的数“要”回来:

// Try to remove some retain counts from the side table.        
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

如果“要”回来的数字大于零,那么将设置 extra_rc 并返回:

// Side table retain count decreased.
// Try to add them to the inline count.
newisa.extra_rc = borrowed - 1;  // redo the original decrement too

否则直接往下执行,向对象发送 -dealloc 消息:

if (performDealloc) {
    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}

参考

Advanced Memory Management Programming Guide

Objective-C 引用计数原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容