OC源码 —— retain和release

retain/release两个关键字现在已经很少见了,但了解一下底层的实现还是能帮助我们更深刻的理解oc的内存管理。


retain

通常情况下,当我们对一个对象调用retain方法时,调用的顺序是这样的:

[NSObject retain];

- (id)retain {
    return ((id)self)->rootRetain();
}

id objc_object::rootRetain()
{
    return rootRetain(false, false);
}

id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); 

        if (slowpath(carry)) {
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

最后这个方法:

id objc_object::rootRetain(bool tryRetain, bool handleOverflow)

看起来有点复杂,但没关系,我会分成几种情况来分析。

在分析之前需要先回顾一下在Runtime源码 —— 对象、类和isa中介绍过isa的结构,其中有这么两个字段:

// 对象的引用计数太大,无法存储
uintptr_t has_sidetable_rc : 1;

// 对象的引用计数超过1,比如10,则此值为9
uintptr_t extra_rc : 8;

这两个字段在上面那个方法中也出现了,从注释来看,一个字段用来存储引用计数的数值,另一个标记引用计数是否溢出。下面就分溢出与否两种情况来讨论。

不溢出

最简单的情况当然是不溢出,这种情况下,rootRetain()方法可以简写如下:

id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    isa_t oldisa;
    isa_t newisa;

    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); 
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    return (id)this;
}

这里面的几个方法都需要解释一下,按照顺序来:

这里的实现版本是x86_64

LoadExclusive
LoadExclusive(&isa.bits)

static uintptr_t LoadExclusive(uintptr_t *src)
{
    return *src;
}

第一个很简单,就是获取isa的内容。

addc
addc(newisa.bits, RC_ONE, 0, &carry)
#       define RC_ONE   (1ULL<<56)

static uintptr_t addc(uintptr_t lhs, uintptr_t rhs, uintptr_t carryin, uintptr_t *carryout)
{
    return __builtin_addcl(lhs, rhs, carryin, carryout);
}

我搜不到__builtin_addcl方法的定义或者说明文档,我只能根据测试结果来做一些猜测。测试的过程是这样的:

addc非溢出测试.png

首先我在rootRetain()方法中添加了两个断点,一个在while内部,一个在外部,运行程序进入第一个断点,获取了一下这个时候isa的内容:

(lldb) p &isa
(isa_t *) $0 = 0x0000600000064f40
(lldb) p *$0
(isa_t) $1 = {
  cls = NSThread
  bits = 8444248074519609
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 17592032694407
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 0
  }
}

方法结束之后,看到carry的值是0。接着运行进入第二个断点,再获取一下isa的内容:

(lldb) p *$0
(isa_t) $2 = {
  cls = NSThread
  bits = 80501842112447545
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 0
    shiftcls = 17592032694407
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 1
  }
}

看到变化了没有,extra_rc的值增加了1。这个时候可以做一些猜测,首先根据isa_t结构体,extra_rc是在第57~64位,RC_ONE就是第57位为1,addc()方法结束之后,extra_rc从0->1,相当于两者相加了,没有溢出,返回的carry值为0。

再测试一下溢出的情况,需要调整一下断点的位置如图:

addc溢出测试.png

进入第一个断点获取一下extra_rc的值,因为获取的时isa的内容,所以还是oldisa的值:

(lldb) p &isa
(isa_t *) $3 = 0x0000000100882a00
(lldb) p *$3
(isa_t) $4 = {
  cls = MTLIGAccelDevice
  bits = 18382989995916412357
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 1
    shiftcls = 553978040
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 0
    extra_rc = 255
  }
}

值为255,加1就会溢出,所以carry的值为1,继续运行进入第二个断点,再获取一下isa:

(lldb) p *$3
(isa_t) $5 = {
  cls = MTLIGAccelDevice
  bits = 9267704350118528453
   = {
    nonpointer = 1
    has_assoc = 0
    has_cxx_dtor = 1
    shiftcls = 553978040
    magic = 59
    weakly_referenced = 0
    deallocating = 0
    has_sidetable_rc = 1
    extra_rc = 128
  }
}

extra_rc从255->128,has_sidetable_rc从0->1,这里边的过程一会儿讲溢出的时候再说。

从这个结果又可以做一点猜测,如果addc的前两个参数加起来溢出了,carry的值就会变化,反正不等于0了,具体怎么变化,是不是存储溢出的值就不得而知了。这个方法就先这样了,大概的意思是懂了。

StoreExclusive
StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)

static bool StoreExclusive(uintptr_t *dst, uintptr_t oldvalue, uintptr_t value)
{
    return __sync_bool_compare_and_swap((void **)dst, (void *)oldvalue, (void *)value);
}

方法内部又调用了一个方法,在gcc的文档中这个方法是这样解释的:

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
...
These builtins perform an atomic compare and swap. That is, if the current value of *ptr is oldval, then write newval into *ptr.
The “bool” version returns true if the comparison is successful and newval was written. 

应用到方法中去,就是如果&isa.bits和oldisa.bits相等,那么就把newisa.bits的值赋给&isa.bits,并且返回true。

在这里&isa.bits和oldisa.bits当然是相等的,所以while判断一次就结束了。

这3个方法讲清楚之后再回去看看简化版的rootRetain()方法就很简单了,其实就是给extra_rc+1,然后更新一下isa的内容。

有溢出

有溢出的时候,情况稍微复杂一点,rootRetain()方法在这个时候会变成这样:

id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    uintptr_t carry;
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); 
    
    if (!handleOverflow) {
        ClearExclusive(&isa.bits);
        return rootRetain_overflow(tryRetain);
    }
}

id objc_object::rootRetain_overflow(bool tryRetain)
{
    return rootRetain(tryRetain, true);
}

方法的前半部分一模一样,但是因为溢出了,所以后面的路线变化了,直接调用了rootRetain_overflow()方法,这个方法内部又调用了rootRetain()方法。

不同的是,首次调用两个参数都是false,而这次调用,第二个参数为true。所以这个时候的rootRetain()方法又可以重新简化一下:

id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    isa_t oldisa;
    isa_t newisa;

    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);

        newisa.extra_rc = RC_HALF;
        newisa.has_sidetable_rc = true;
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    sidetable_addExtraRC_nolock(RC_HALF);

    return (id)this;
}
#       define RC_HALF  (1ULL<<7)

奇怪的是,看看完整版的rootRetain()方法,第二个参数并没有其他用处,为什么需要绕个弯再调用一下自己,而不是直接继续执行后面的代码呢?

源代码中对此的注释只有:

// handleOverflow=false is the frameless fast path.
// handleOverflow=true is the framed slow path including overflow to side table
// The code is structured this way to prevent duplication.

但是就从源代码来看,直接将这个参数去掉,rootRetain()方法只留下第一个参数都是可以的。甚至还减少了函数调用的次数。

先不管这个疑问,这一次,更新了newisa的extra_rc和has_sidetable_rc字段,将extra_rc设置为了RC_HALF也就是128,has_sidetable_rc设为true,这就是为什么上面测试溢出时输出结果为:

extra_rc = 128
has_sidetable_rc = 1

就是在此处设置的。

这里就有一个疑问了,讲道理这个时候extra_rc应该为256,这里值为128,还有128哪里去了?

这就是最后那个方法做的事情了:

sidetable_addExtraRC_nolock(RC_HALF);

bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    SideTable& table = SideTables()[this];

    size_t& refcntStorage = table.refcnts[this];
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    uintptr_t carry;
    size_t newRefcnt = addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    if (carry) {
        refcntStorage = SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  
#define SIDE_TABLE_RC_ONE            (1UL<<2)
#define SIDE_TABLE_RC_SHIFT 2

SideTable看起来就是用来存储引用计数的。看结构体SideTable的源码,应该和weak reference也有点关系,以后研究weak的时候再深入吧,目前我也不是很清楚。

现在只需要注意到在获取到oldRefcnt之后,紧跟着两个assert,从宏的定义来看,分别是第一和第二位,assert是为了确保oldRefcnt低两位为0,因为低两位是有作用的标志位。也就是说引用计数实际上是从第三位开始存放的。

再向后看,这就解释了addc()第二个参数左移了两位进行累加了。

这里再一次进行了溢出判断,如果溢出了,存放的引用计数被置为了SIDE_TABLE_RC_PINNED,因为(oldRefcnt & SIDE_TABLE_FLAG_MASK)结果一定是0,但是我不理解这里为什么要这么做。

同样不能理解的还有addc之前的if判断。是不是引用计数太大,就不处理了呢?毕竟有62位用来存,这要是再溢出了就无法想象了。

验证一下正常的结果:

addExtraRC不溢出.png

传入的参数是RC_HALF,也就是128,在addc之后,newRefcnt变为512 = 128 << 2也没问题,carry为0没有溢出,最后更新了一下存放的引用计数值。

这里稍微汇总一下,正常情况下,retain方法就是给extra_rc加1,当extra_rc溢出时,将一半的引用计数存放到SideTable中。

一个合理的疑问是,当extra_rc再次溢出的时候呢?很容易测试,只需要再进一次断点,如下图:

同一个对象extra_rc第二次溢出.png

这个时候oldRefcnt已经是512 = 128 << 2,也就是上一步存进去的结果,addc之后又加了128进去。说白了每次extra_rc溢出了,SideTable中就增加128。

到这里,关于retain方法的源码已经看的差不多了,但是还有疑问:

为什么每次只移动一半的引用计数(也就是128)到SideTable中?

我也不知道。


release

在深入理解了retain之后,再看release的源码就很简单了。代码太长我就不贴了,总结一下release的流程:

  1. 最普通的情况,直接将extra_rc减1
  2. 如果extra_rc为0,判断has_sidetable_rc
  3. has_sidetable_rc = false,说明对象已经没有引用计数了,直接dealloc释放内存
  4. has_sidetable_rc = true,说明extra_rc有过溢出
  5. 从SideTable中借位成功,每次取RC_HALF,也就是128,减1之后赋给extra_rc,回到步骤1
  6. 从SideTable中借位失败,直接dealloc

retainCount

最后再说一下和retain有关的一个方法,就是获取引用计数值retainCount,这个方法其实不看源码也能想象出来是怎么计算的:

retainCount = 1+ extra_rc + SideTable中存储的rc

源码贴一下作为结尾:

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

inline uintptr_t 
objc_object::rootRetainCount()
{
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

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

推荐阅读更多精彩内容