OC底层原理初探之内存管理

前言

我们大家都知道,iOS是使用引用计数对对象内存进行管理,alloc后引用计数为1,对对象发送retain消息时,引用计数会进行+1,发送release消息时进行-1。当引用计数为0时会自动对对象进行销毁。那么 retainreleasedealloc这些方法在底层做了一些什么呢?带着这样的问题,我们开始今天的探索。

retain方法解析:

要探索retain,我们最后使用MRC项目,能清晰调用流程

选中项目Target,选择Build Settings 搜索automatic,然后将如下图所示的选项修改为NO

image.png

按上面操作后,我们的下面已经是MRC了。如下声明一个XQPerson类:

@interface XQPerson : NSObject

@end

@implementation XQPerson

@end
复制代码

main.m调用:

image.png

程序运行起来后打开汇编调试,如下图所示

image.png

我们发现,并没有按预想的调用retain方法,反而调用了objc_retain函数,参考alloc方法被替换为objc_alloc函数,我们可以推断出这又是编译器将retain替换为objc_retain函数

objc_retain函数解析

id 
objc_retain(id obj)
{
    if (obj->isTaggedPointerOrNil()) return obj;
    return obj->retain();
}
复制代码
  • 判断是否taggedPointer类型,如果是则直接返回,不是则调用retain函数

objc_object::retain函数:

inline id 
objc_object::retain()
{
    ASSERT(!isTaggedPointer());

    return rootRetain(false, RRVariant::FastOrMsgSend);
}
复制代码
  • 直接调用了objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)函数,并传参 falseFastOrMsgSend

objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)函数(重点)解析:

ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_retain()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {//判断当前类或父类(除NSObject外)是否重写了retain等方法
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                return swiftRetain.load(memory_order_relaxed)((id)this);
            }
            return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return (id)this;
        }
    }

    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);//表示isa的ExtraRC已装满,再次调用此方法,传入variant = RRVariant::Full
            }
            // 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;
        }
        // 比较 isa.bits 和 oldisa.bits 如果相等,将 newisa.bits赋值给 isa.bits并退出循环
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (variant == RRVariant::Full) {
        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();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }

    return (id)this;
}
复制代码
  • variant == RRVariant::FastOrMsgSend,此时判断为真,oldisa.getDecodedClass(**false**)->hasCustomRR() 判断当前类或父类(除NSObject外)是否重写了retain等方法,如果是则重新调用当前对象的retain方法。
  • !oldisa.nonpointer判断是否nonpointer(通常类对象都是nonpointer,实例对象除非配置了环境变量才有可能为真,此时判断如果是类对象则直接返回
  • 进入do while循环,首先判断是否nonpointer,如果!newisa.nonpointer为真,执行sidetable_retain,直接使用散列表进行存储
  • newisa.isDeallocating()判断是否正在释放,如果是则直接返回
  • newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);isaextra_rc进行+1RC_ONE根据架构不同会有差异,以arm64为例,此时为1ULL<<45isa结构中extra_rc从第45位开始,工占用19位,所以此时对newisa.bits进行加1ULL<<45即为对extra_rc1,当超出isa.bits范围是,carry会被赋值
  • slowpath(carry)判断isa是否存满,variant != RRVariant::Full,此时variantRRVariant::FastOrMsgSend,进入此判断,调用rootRetain_overflow函数,而rootRetain_overflow实际上会再次调用objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)函数,并传入参数 falseFull
  • 当调用rootRetain_overflow函数再次进入当前函数后,继续执行下面的代码newisa.extra_rc = RC_HALF(1ULL<<18);extra_rc设置为满载的一半,newisa.has_sidetable_rc = true; 设置has_sidetable_rc1,isa标记使用了sidetable存储引用计数
  • slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)) 比较 isa.bits 和 oldisa.bits 如果相等,将 newisa.bits赋值给 isa.bits并退出循环
  • sidetable_addExtraRC_nolock(RC_HALF);将另一半引用计数存入散列表

rootRetain_overflow函数解析:

NEVER_INLINE id 
objc_object::rootRetain_overflow(bool tryRetain)
{
    return rootRetain(tryRetain, RRVariant::Full);
}
复制代码
  • 实际重新调用了rootRetain函数并传值variant = RRVariant::Full

sidetable_addExtraRC_nolock函数解析:

bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    ASSERT(isa.nonpointer);
    // 取出散列表
    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;
    // 散列表引用计数 + RC_HALF(1 << 18) << 2,由于从第三位开始才存储引用计数,所以实际引用计数增加了 1 << 18个
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
        //判断是否超出
    if (carry) {
        //如果超出则将 修改refcntStorage,再次进入此函数时oldRefcnt & SIDE_TABLE_RC_PINNED为true则会直接返回
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        // refcntStorage 赋新值
        refcntStorage = newRefcnt;
        return false;
    }
}
复制代码
  • SideTable& table = SideTables()[this];取出散列表
  • size_t& refcntStorage = table.refcnts[this];取出引用计数,size_t&表示直接对指向区域进行赋值
  • oldRefcnt & SIDE_TABLE_RC_PINNED判断散列表是否已满,如满直接返回
  • addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry)oldRefcnt增加RC_HALF(1 << 18) << 2,由于是散列表引用计数从第3位开始存储,所以实际实际引用计数增加了 1 << 18个,即为isa.extra_rc容量的一半。
  • if (carry)判断是否已装满,如已装满,增加装满的标识并返回

retain方法小结

当对一个对象发送retain消息时,会优先将引用计数存储在isa.extra_rc,当超出能存储的范围时,将需要借助散列表进行存储,此时会将一半的引用计数存入散列表

release方法解析

release方法的探索和retain类似,只需要将调用 retain 改为 release即可

image.png

继续查看汇编,显示如下:
image.png

由汇编分析可知,releaseretain一样,也被替换成了objc_release

objc_release函数解析:

void 
objc_release(id obj)
{
    if (obj->isTaggedPointerOrNil()) return;
    return obj->release();
}
复制代码
  • 判断是否taggedPointer,如果是直接返回
  • 调用release函数

objc_object::release函数解析:

inline void
objc_object::release()
{
    ASSERT(!isTaggedPointer());

    rootRelease(true, RRVariant::FastOrMsgSend);
}
复制代码
  • 直接调用rootRelease函数并传参performDealloc = true,variant = RRVariant::FastOrMsgSend
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;

    bool sideTableLocked = false;

    isa_t newisa, oldisa;

    oldisa = LoadExclusive(&isa.bits);
    // 判断是否 FastOrMsgSend
    if (variant == RRVariant::FastOrMsgSend) {
        // These checks are only meaningful for objc_release()
        // They are here so that we avoid a re-load of the isa.
        //判断是否重写release等方法
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return false;
        }
    }

retry:
    do {
        newisa = oldisa;
        //判断是否nonpointer,如果不是,则表示只使用了散列表存储引用计数,直接处理散列表的引用计数
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        //判断是否正在释放
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }

        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        //对isa.extra_rc进行--
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {// 不能再减了跳转 underflow
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
    //引用计数进行处理后,判断是否释放,如果引用计数为0 则跳转 deallocate
    if (slowpath(newisa.isDeallocating()))
        goto deallocate;

    if (variant == RRVariant::Full) {
        if (slowpath(sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!sideTableLocked);
    }
    return false;

 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 (variant != RRVariant::Full) {
            ClearExclusive(&isa.bits);
            // 再次调用 rootRelease函数并传入 variant = RRVariant::Full
            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.
            oldisa = LoadExclusive(&isa.bits);
            goto retry;
        }

        // Try to remove some retain counts from the side table.        
        // 从散列表中取出RC_HALF(1 << 18)个引用计数
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);
        // borrow.remaining表示散列表中是否还有引用计数
        bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there
        // 如果取出了引用计数
        if (borrow.borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            bool didTransitionToDeallocating = false;
            //对 newisa.extra_rc进行赋值,将 borrow.borrowed - 1个引用计数存入newisa.extra_rc
            newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement too
            // 标记是否还使用散列表存储引用计数
            newisa.has_sidetable_rc = !emptySideTable;
            // 判断 isa.bits 和 oldisa.bits是否相等,如果相等,将 newisa.bits赋值给 isa.bits 并返回true
            bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
            // 判断是否保存成功,如果未成功,则对oldisa进行修改
            if (!stored && oldisa.nonpointer) {
                // 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.
                uintptr_t overflow;
                //对 oldisa.bits进行 的引用计数++后再次赋值给 newisa.bits
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                // 判断是否存满,未存满再次更新isa
                if (!overflow) {
                    stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
                    if (stored) {
                        didTransitionToDeallocating = newisa.isDeallocating();
                    }
                }
            }
            // 判断是否存储失败了
            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                ClearExclusive(&isa.bits);
                如果失败了,将取出的引用计数再次存入散列表
                sidetable_addExtraRC_nolock(borrow.borrowed);
                oldisa = LoadExclusive(&isa.bits);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            if (emptySideTable)
                sidetable_clearExtraRC_nolock();

            if (!didTransitionToDeallocating) {
                if (slowpath(sideTableLocked)) sidetable_unlock();
                return false;
            }
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

deallocate:
    // Really deallocate.

    ASSERT(newisa.isDeallocating());
    ASSERT(isa.isDeallocating());

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        //发送dealloc消息
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}
复制代码
  • rootRelease函数和rootRetain函数有很多相似之处,相同的判断逻辑这里就不赘述了,重点说明一下underflowdeallocate流程
  • slowpath(newisa.has_sidetable_rc)判断散列表中是否有引用计数,如果有,调用rootRelease_underflow函数,再次调用rootRelease并传入 variant = RRVariant::Full
  • auto borrow = sidetable_subExtraRC_nolock(RC_HALF);从散列表中取出引用计数,bool emptySideTable = borrow.remaining == 0散列表中是否还有引用计数
  • newisa.extra_rc = borrow.borrowed - 1extra_rc赋值为borrow.borrowed - 1(由于此时是release所以 -1newisa.has_sidetable_rc = !emptySideTable;标记是否使用散列表存储引用计数
  • bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);,判断 isa.bits 和 oldisa.bits是否相等,如果相等,将 newisa.bits赋值给 isa.bits 并返回true
  • if (!stored && oldisa.nonpointer)判断是否存储成功,如果未成功,且为nonpointer,再次使用oldisa.bits进行存储,
  • if (!stored)判断是否存储成,如果未成功,则将引用计数再次存入散列表
  • deallocate 如果引用计数为0,则直接发送dealloc消息,将对象进行释放

release方法小结

当对一个对象发送release消息时,首先会将isa.extra_rc -1,当isa.extra_rc0时,判断散列表中是否有引用计数,如果有,从散列表中取出extra_rc容量一半的引用计数存入extra_rc-1,当extra_rc和散列表中都没有引用计数了就对对象发送一个dealloc消息,将对象释放

dealloc方法分析:

通过对release方法的探索,我们已经知道了当对象引用计数为0时会直接对对象发送dealloc消息,我们打开dealloc方法的源码如下:

- (void)dealloc {
    _objc_rootDealloc(self);
}
void

_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}

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

    if (fastpath(isa.nonpointer                     &&
                 !isa.weakly_referenced             &&
                 !isa.has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &&
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
复制代码
  • 封装层级比较深,顺序依次为 dealloc -> _objc_rootDealloc -> rootDeallocrootDealloc 判断是否为nonpointer,同时没有被弱引用持有,没有关联对象,没有Cxx析构函数,没有使用散列表存储引用计数,则直接free

object_dispose函数解析:

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

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
复制代码
  • obj不为空直接调用objc_destructInstance

objc_destructInstance函数解析:

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

        // This order is important.
        if (cxx) object_cxxDestruct(obj);// 清除成员变量(strong等修饰)
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }

    return obj;
}
复制代码
  • 判断是否有cxx析构函数,如果有则调用object_cxxDestruct,清除成员变量
  • 判断是否有关联对象,如果有调用_object_remove_assocations,清除关联对象
  • clearDeallocating清除和对象相关的弱引用表

object_cxxDestruct函数解析:

分析此函数,要先分析,在什么时候bool cxx = obj->hasCxxDtor()true,首先还是之前定义的XQPerson,添加一个int age属性如下所示

@interface XQPerson : NSObject
@property(nonatomic,assign)int age;
@end
复制代码

运行起来后,发现rootDealloc函数已经直接释放了,所以可以确定,此时没有cxx析构函数

XQPerson类进行修改,增加一个strong修饰的name属性如下:

@interface XQPerson : NSObject
@property(nonatomic,copy)NSString* name;
@end
复制代码

运行起来后发现, bool cxx = obj->hasCxxDtor()为true,由此可以确定,有strong修饰的属性的类的实例对象有 cxx 析构函数,按照此方法,分别验证了 retaincopyweak等修饰的属性都存在cxx 析构函数,而只有assignunsafe_unretained修饰属性的对象则没有

object_cxxDestruct函数解析:

void object_cxxDestruct(id obj)
{
    if (obj->isTaggedPointerOrNil()) return;
    object_cxxDestructFromClass(obj, obj->ISA());
}
复制代码
  • 直接调用object_cxxDestructFromClass函数
static void object_cxxDestructFromClass(id obj, Class cls)
{
    void (*dtor)(id);

    // Call cls's dtor first, then superclasses's dtors.

    for ( ; cls; cls = cls->getSuperclass()) {
        if (!cls->hasCxxDtor()) return; 
        // 查找缓存和方法列表
        dtor = (void(*)(id))
            lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
        if (dtor != (void(*)(id))_objc_msgForward_impcache) {
            if (PrintCxxCtors) {
                _objc_inform("CXX: calling C++ destructors for class %s", 
                             cls->nameForLogging());
            }
            //获取到imp后执行
            (*dtor)(obj);
        }
    }
}
复制代码
  • 通过调试 SEL_cxx_destruct = .cxx_destruct,通过添加符号断点-[XQPerson .cxx_destruct],由实测发现strongretaincopy修饰的属性,如下所示:

image.png

从上图可以得知,会执行objc_storeStrong函数

objc_storeStrong函数分析:

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}
复制代码
  • 此时传入objnil,由此可见,实际是将属性指向nil,并执行一次release

weak修饰的属性则有所不同,如下图所示:

image.png

从上图可知,weak修饰的属性会调用objc_destroyWeak函数,对弱引用进行置空

dealloc方法小结

  • dealloc方法主要做的事情是调用object_cxxDestruct函数清空对象的成员变量,_object_remove_assocations函数清空对象的关联对象,clearDeallocating函数清空对象的散列表
  • 需要注意的,在ARC重写dealloc会自动调用父类的dealloc方法,但是在MRC并不会自动调用父类的dealloc方法,所以MRC开发时,若重写了dealloc方法,必须要调用父类的dealloc方法。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享