iOS 内存管理2-MRC&ARC,retain、release、dealloc

oc和swift都是使用引用计数进行管理对象。 每个对象的引用计数是如何存储的?

isa是一个64位的联合体位域,根据CPU架构的不同每个成员的长度不同。
isa结构
isa指针中的extra_rc用于存放引用计数,当引用计数达到一定值时会存储到散列表中。
在objc4-781源码中,查找源码
objc_setProperty->reallySetProperty->objc_retain->retain->rootRetain
其中reallySetProperty主要是对新值的retain,旧值的release。

retain

retain主要工作:

    1. 判断是否为taggedPointer类型,是返回自身,
    1. 如果不是taggedPointer类型,操作引用计数
    • 2.1 判断是否为nonpointer类型,不是则直接操作SideTables散列表,返回散列表中的内容
    • 2.2 判断是否正在释放,如果正在释放,则执行dealloc流程
    • 2.3 执行引用计数extra_rc+1操作,并返回引用计数isa->extra_rc的状态标识carry,用于判断extra_rc是否满了
    • 2.4 如果extra_rc中的引用计数满了,此时需要操作散列表,将满状态的引用计数一半存到extra_rc,另一半存在散列表的rc_half中。
      如果都存储在散列表,每次对散列表操作都需要开解锁,操作耗时,消耗性能大,此操作的目的在于提高性能

nonpinter: 表示是否对 isa 指针开启指针优化,

  • 0:纯isa指针,
  • 1:经过优化后的isa指针

理解slowpathfastpath,用于在编译器编译时判断是否需要进行指令优化,如果是fastpath会让cpu提前预读该语句下分支为1的一条指令,从而减少指令跳转带来的开销。
简单理解fastpath执行该代码的概率大,从而让cpu提前做好准备。slowpath执行该代码的概率小。

源码如下

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
  //1.判断是否为taggedpointer类型
    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;
        //2.1 不是nonpointer类型,直接操作散列表
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return (id)this;
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        //2.2 判断是否正在释放
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        //2.3 extra_rc+1操作,carry表示是否慢
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
      //2.4 如果满了,
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
          // 如果不处理溢出情况,则在这里会递归调用一次,再进来的时候,handleOverflow会被rootRetain_overflow设置为true,从而进入到下面的溢出处理流程
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } 
    // 将oldisa 替换为 newisa,并赋值给isa.bits(更新isa_t), 如果不成功,do while再试一遍
    while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
    //将extra_rc中一半存入散列表
    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

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

具体流程图如下:


image.png

散列表

oc将isa中满了的引用计数存储在散列表中,散列表有多张,ios有8张,macos有64张,
操作散列表会进行锁操作会有开销,如果所有的数据都在一张表不安全,而每个对象都有一个散列表开销更大,控制在8张。每个散列表中有refcnts引用计数表(哈希表)存储每个对象的引用计数。
散列表的结构如下

struct SideTable {
    spinlock_t slock;            //自旋锁
    RefcountMap refcnts;     //引用计数表
    weak_table_t weak_table;   //弱引用表
    ...
};

使用散列表的原因

  • 数组:特点数据的是读取快,存储不方便
  • 链表:增删方便,查询慢(需要从头节点开始遍历查询)
  • 散列表:本质就是一张哈希表,哈希表集合了数组和链表的长处,增删改查都比较方便

release

根据retain原理,release进行减一操作。

    1. 判断是否为TaggedPointer类型,是就返回false
    1. 如果不是TaggedPointer,操作引用计数
    • 2.1 判断是否nonpointer,如果是纯指针(即值为0),对散列表中的引用计数减一
    • 2.2 如果nonpointer是1,extra_rc--,并返回carry状态
    • 2.3 根据carry判断,extra_rc是否还有引用计数,carry为1表示extra_rc无值,跳转到underflow中处理散列表。
    • 2.4 extra_rc还有引用计数,则返回false
  • 3 underflow中处理散列表
    • 3.1 如果has_sidetable_rc为true,取出散列表中还有一半的引用计数-1,并存储到extra_rc
    • 3.2 如果performDealloc 为true,则执行delloc
ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
  //判断是否为taggedPointer类型
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //纯指针类型,操作散列表
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return false;
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        //引用extra_rc--操作
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--       
        //如果extra_rc不存在值了,跳转underflow中
        if (slowpath(carry)) {
            // don't ClearExclusive()
    
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;
//处理extra_rc中没有引用计数的情况
 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;
    //判断是否关联散列表
    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.
       //散列表开锁
        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.       
      //取出散列表的引用计数 
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

    // Really deallocate.

    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

dealloc

释放对象需要判断isa指针各成员的值
nonpointer: 是否为优化过的指针,为1
weakly_referenced: 弱引用表,为0
has_assoc:是否存在关联对象,为0
has_cxx_dtor:是否有C++析构函数,为0
has_sidetable_rc: 是否关联散列表,为0

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

由于这里使用了fastPath,dealloc过程绝大多数情况都是在上述条件成立的情况下自动释放对象。

查看object_dispose源码

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        //判断是否有c++析构函数
        bool cxx = obj->hasCxxDtor();
      //判断是否有关联对象
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        
        obj->clearDeallocating();
    }

    return obj;
}

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
  //判断弱引用表,散列表是否有值
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

CFGetRetainCount

获取对象的引用计数的数量

    NSObject *obj  =[NSObject alloc];
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));

输出


image.png

alloc主要做了三件事


image.png
  • 1.计算对象的内存占用空间
    1. 分配内存空间
    1. 初始化isa指针,

alloc并没有对retain加一操作,查看objc4-781retainCount源码
retainCount->_objc_rootRetainCount->rootRetainCount

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

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
      //此时读取extra_rc 加一后判断散列表中是否有值,然后返回rc,此过程并没有读写操作
        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();
}

此时读取extra_rc加一后判断散列表中是否有值,然后进行操作后返回rc,此过程并没有写操作
而在最新的objc818.2源码中取消掉了加一操作,可以猜测ios14系统还没使用objc818的源码。

总结:

    1. retain:retain并不是简单的对isa->extra_rc加一,如果extra_rc满了会存储一半的值到散列表。
    1. release:isa->extra_rc减一,如果extra_rc值为空再去判断散列表中查找,散列表有值将其存入extra_rc,没有等待释放
    1. dealloc:释放时判断弱引用表,关联对象,散列表,C++析构
    1. retaincount:读取isa->extra_rc后加一,在去查找散列表中是否有值
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,999评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,102评论 1 302
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,709评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,439评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,846评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,881评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,062评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,783评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,517评论 1 248
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,762评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,241评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,236评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,145评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,941评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,965评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,802评论 2 275

推荐阅读更多精彩内容