所有代码基于 objc 818.2

说到 OC 这个语言,一般都会说,OC 是 C 的超集实现,在 C 的基础上增加了 runtime 机制,使得 OC 成为一个动态语言,同时也增加了一些语言特性,比如增加了类的概念,同时也增加了一些内存管理方面的功能。

而对于内存管理,一般就是指 OC 的对象引用计数,这中间又分为两种,MRC(手动引用计数)、ARC(自动引用计数),目前使用的更多的还是 ARC 吧。说到引用,下一步就要说到引用的类型,比如在 OC 中就支持强引用和弱引用两种,一般没有特殊标记,默认使用的就是强引用,这一点在声明 property 时有比较明显地体现,比如我们经常需要区分,strong、weak、assign 这三种不同类型的 property 的实际表现。简单来说,

  • strong 表示这是一个强引用,当给这个属性赋值时,对象的引用计数就会 +1
  • weak 表示这是一个弱引用,给属性赋值不会增加对象的引用计数,它与 assign 的主要区别就是,当对象引用计数为 0 被销毁时,这个 weak 属性也会同时被置为 nil
  • assign 可以简单理解为 C 中指针的概念,对它赋值不会影响到 OC 的引用计数,同时也不会被引用计数影响,所以如果使用 assign 类型属性保存 OC 的变量,就很容易出现野指针,这是不推荐的

引用计数原理

OC 区分强引用的一个底层基础,就是它的引用计数机制,只有强引用才会增加对象的引用计数,而弱引用则是增加对象的弱引用计数,而当一个对象的强引用计数为 0 时,便会执行对象的 dealloc 方法,并对弱引用进行清理,下面简单看下 OC 的引用计数。

另外,OC 的引用计数就会涉及到 OC 的自动/手动引用计数,也就是常说的 MRC、ARC,ARC 的基本原理就是,OC 在编译的时候会自动地调用 retain/release 等方法,完成计数管理,由于 MRC 已经是很久之前的产物的,下面会主要讲 ARC 下的相关实现。

一般来说,当我们执行以下代码时,就会触发相关的计数函数:

// NSObject *obj1;
// NSObject *obj2;

obj1 = obj2;

它一般就会触发 objc_storeStrong,

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

就是两个过程,先对 obj2 指向的对象引用计数+1(有一个新的指针,obj1 指向了 obj2 指向的对象),然后对 obj1 指向的对象引用计数 -1(原本指向这个对象的 obj1 指向了其他对象),下面看下具体的计数函数。

引用计数 +1,objc_retain

id 
objc_retain(id obj)
{
    if (obj->isTaggedPointerOrNil()) return obj;
    return obj->retain();
}

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

    return rootRetain(false, RRVariant::FastOrMsgSend);
}

这里会先判断一下对象是否为空或是否是一个 tagged pointer,如果是,意味着这不是一个有效的 OC 对象,则直接跳过。然后调用 rootRetain,同时需要注意,参数 variant 为 FastOrMsgSend,他还有另外两种,分别为 Full 和 Fast,这个参数在更改引用计数的过程中还是挺关键的,这里就先大致描述一下,更新引用计数的几种类型。

首先,与调用类型 Full 相对应的是 Fast,他们的区别主要在于这是否是一个“完全的”计数更新,这个是否“完全”主要是根据我们需要调整哪一块的计数来区分的,关于引用计数,最起码的一个条件就是,需要有一个地方存放计数信息,即一个对象到底有多少个引用,这就需要考虑用什么数据结构去存这个数字,用多大的数据结构去存,如果给的空间大了,就会造成很多的内存浪费,而且读起来也费劲,如果给小了,可能稍微大型的程序计数数量就溢出了,所以作为一个高效的运行框架,OC 的 runtime 是这么做的,首先,在 isa 中给出一部分字节存放计数信息,但不会给太多,当这部分字节不足以存放太大的数字时,就用一个额外的数据结构,SideTable(可以认为是一个哈希表) 去存放计数个数,从而达到分担存储压力的作用。

所以 Full 与 Fast 类型的区别就是,做计数变动的时候,是直接在 isa 提供的一部分字节上改动,还是要到 SideTable 中去改动,一般情况下直接改动 isa 的字节肯定是更快的,所以叫 Fast,而需要动用 SideTable 的会慢些,叫 Full。相关的数据结构可以参考下面 isa 的结构定义,其中 extra_rc 即为 isa 中存储计数的结构,而 has_sidetable_rc 则标志着是否动用了 SideTable 去存储额外的引用计数。

#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

另外一个就是 FastOrMsgSend 和 Fast 的区别,他们的主要区别是是否带有“MsgSend”,这个应该是在 ARC 下,自动生成 objc_storeStrong 的调用时,如果某些类型,可能会有自己的 retain 方法实现,所以在 rootRetain 中,就会检查这个对象的类是否有自己的 retain 方法实现,如果有的话则优先调用对应的 OC 方法,而不是按照系统默认的 rootRetain 来执行。

以上就是大致的计数原理,下面看 rootRetain 的实现:

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);

  // 如果对象有自己的 retain 实现,就调用对应的 OC 方法
    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())) {
            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));
        }
    }

  // 如果这不是一个有效的 OC 对象,就跳过
    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;
        }
    }

  // 循环执行计数操作,循环的目的在于保证多线程安全,while 中的 StoreExclusive
  // 会判断 oldIsa 与 newIsa 是否符合预期,如果不,就重新拿到最新的 isa 做计数 +1
    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;
            }
        }
      
      // 判断是否可以直接加到 isa 的结构中,
      // 如果可以,carry 为 0
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

      // 当 carry 不为 0,表示 isa 中的 extra_rc 已经满了,需要借助额外的 SideTable
      // 存储计数信息,此处会更改 extra_rc 和 has_sidetable_rc 状态,
      // 比较有意思的是,extra_rc 是直接减半,这点后面说明
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                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;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

  // 如果需要借助 SideTable 存储,则调用 sidetable_addExtraRC_nolock
  // 将一般的计数信息存到 SideTable 中
    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;
}

这里有一点比较重要,当 extra_rc 已经加满了,需要进位到 SideTable 时,这里的操作是,将一般的 extra_rc 加到 SideTable 中,另一半还是保留在 extra_rc 中,个人感觉,这就是一个极致优化的体现。

为什么不全都移到 SideTable 中存储,而是保留一半?还是要提到,进行 SideTable 操作是一个比较重的操作,理想的状态是只进行 extra_rc 的更改,那么保留一半的好处就是,进行了一次 SideTable 之后,不管是该对象的计数在短时间内迅速增加,还是短时间内迅速减少,还是不断地小范围波动,都不会再触发 SideTable 操作,而如果是直接将 extra_rc 全部移到 SideTable,如果之后立刻对引用计数 -1,就需要再操作 SideTable 才行,这就会导致很明显的性能问题。

然后就是关于 SideTable 本身,这就是一个哈希表的实现,它内部通过 refcnts 存储了引用计数信息,weak_table 存储了弱引用相关的信息,然后还有一个 slock 用于加锁,所以这个数据结构本身可以认为是计数管理中一个用于辅助计数的结构吧。不过在 SideTable 中,OC 也只是使用了一个 size_t 去存储某个对象的引用计数个数,所以从这个角度看,即便是使用 SideTable 辅助存储计数,依旧有溢出的可能,不过这个很难达到就是了。

引用计数 -1,objc_release

引用计数 -1 就是 +1 的一个逆过程,对应的函数调用为 objc_release,

void 
objc_release(id obj)
{
    if (obj->isTaggedPointerOrNil()) return;
    return obj->release();
}

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

    rootRelease(true, RRVariant::FastOrMsgSend);
}

这里最终调用 rootRelease,传的参数依旧是 FastOrMsgSend,它的作用与在 rootRetain 中一致。然后,rootRelease 的功能,

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);

  // 如果对象有 release 方法的实现,则优先调用 release 方法
    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.
        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;
        }
    }

  // 依旧是 do while 循环进行多线程安全的计数 -1
retry:
    do {
        newisa = oldisa;
        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;
        }

      // 当 extra_rc 为 0 时,进入 underflow 状态
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (slowpath(newisa.isDeallocating()))
        goto deallocate;

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

  // underflow 状态,判断是否有采用 SideTable 存储计数信息,如果用,则从中取出一部分计数,放到 extra_rc 中,然后对 extra_rc -1
  // 关于 SideTable 的使用,就类似进位的概念,如果 extra_rc 溢出了,就进位到 SideTable 中,如果 extra_rc 不够了,就从 SideTable 借位。
 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);
            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;
        }

      // 从 SideTable 借出一半的计数,并记录 SideTable 中还有多少计数
        // Try to remove some retain counts from the side table.        
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

        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;  // redo the original decrement too
            newisa.has_sidetable_rc = !emptySideTable;

            bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);

            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;
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                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.
        }
    }

  // 进入对象销毁状态,如果对象还有计数,在上面就会直接返回了,否则会进入这个阶段,
  // 调用对象的 dealloc 方法,完成销毁。
deallocate:
    // Really deallocate.

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

    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 方法实现的,相关的调用顺序为:

- (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);
    }
}

最终,在 rootDealloc 中,会判断对象是否有弱引用、是否有 SideTable 的引用计数的等,决定是调用 object_dispose 还是直接 free 掉对象,后一种方式是更快的路径。而在 object_dispose 中,会依次清理掉对象的关联对象、弱引用、SideTable 中的存储空间等,一个比较完整的对象销毁过程。

弱引用实现原理

关于弱引用的使用,可以参考下面类的实现:

@interface WRWeakTest ()

@property (nonatomic, weak) NSObject *weakObj;

@end

@implementation WRWeakTest

- (void)wrTest {
    {
        NSObject *obj = [NSObject new];
        self.weakObj = obj;
        id __weak w = obj;
        NSLog(@"");
    }
  
    [self wrTest2];
}

- (void)wrTest2 {
    __strong typeof(self.weakObj) strongObj = self.weakObj;
    NSLog(@"");
}

@end

弱引用创建过程

首先看下,一个弱引用的生成过程。以上代码中有两种弱引用的使用,一种是 weak 类型的 property,另一种是直接在代码中通过__weak实现弱引用,然后在 wrTest 中对它们执行赋值操作,通过运行时打断点便可以看到以上语句在运行时会调用什么函数,对于 wrTest 方法,它的汇编命令为:

image-20210803231233187

于是可以看到,当给 self.weakObj 赋值时,调用了 objc_storeWeak 函数,当直接用__weak初始化一个弱引用时,调用的是 objc_initWeak,这两个函数的区别本身其实不太大,

id
objc_storeWeak(id *location, id newObj)
{
  // 对于给 weakObj 赋值场景,需要考虑它是否已经指向了某个对象,此时需要先做一些解除
  // 与旧的对象的绑定关系
    return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object *)newObj);
}

id
objc_initWeak(id *location, id newObj)
{
  // 而对于新声明的弱引用,则需要考虑 newObj 是否为 nil,
  // 但可以不考虑这个引用是否已经指向某个对象(DontHaveOld)
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

主要就是两种不同的调用场景,做了特定的优化,最终都是需要调用 storeWeak 进行弱引用的关系绑定。在 storeWeak 函数中,整体的流程约等于:

  1. 判断弱引用当前指向对象,并取出对象的 SideTable
  2. 取出新对象的 SideTable
  3. 如果新对象的 Class 还未完成初始化,则先进行初始化,然后重新开始步骤 1
  4. 将弱引用与其当前指向的对象解除关系,主要是操作 SideTable
  5. 将弱引用与新对象的 SideTable 建立关系
  6. 触发新对象的 _setWeaklyReferenced 回调(如果有)

具体代码实现:

template <HaveOld haveOld, HaveNew haveNew,
          enum CrashIfDeallocating crashIfDeallocating>
static id 
storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            class_initialize(cls, (id)newObj);

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (!newObj->isTaggedPointerOrNil()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    // This must be called without the locks held, as it can invoke
    // arbitrary code. In particular, even if _setWeaklyReferenced
    // is not implemented, resolveInstanceMethod: may be, and may
    // call back into the weak reference machinery.
    callSetWeaklyReferenced((id)newObj);

    return (id)newObj;
}

弱引用存储结构

在这个过程中要着重说一下这中间涉及到的存储结构,也就是 SideTable,首先取一个对象的 SideTable 用的函数是&SideTables()[obj],SideTables() 的类型是 StripedMap,可以将其理解成是一个哈希表结构,每一个对象理解成 key,那 value 就是 SideTable,SideTables()[obj]这个过程,就是先求出 obj 的 hash 值,然后索引到一个具体的 SideTable 的过程,而 SideTable 就是一个处理冲突的数据结构,比较有趣的是,SideTable 本身也是一个哈希表,它里面再通过另一种方法对 obj 求 hash,进行索引,然后采用线性探测处理冲突。具体的实现就是下面这里,从一个 SideTable 中查找 obj 对应的 weak_entry_t:

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    ASSERT(referent);

    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

weak_entry_t 里存放的,是一个对象的所有弱引用,而它也是一个存储结构,也同样针对数据存取做了一些优化,简单来说就是,weak_entry_t 中有两个数据结构,一个长度为 4 的数组 inline_referrers 和一个哈希表 referrers,当一个对象的弱引用数量不大于 4 时,就直接使用数组存储,而当数量大于 4 时,就启用哈希表 referrers 进行高效存储。

所以,根据以上信息,总的来说,OC 的弱引用实现中使用了三个哈希表做存储,在实际看代码了解其原理的过程中,在这部分花的时间算是最多的了,下面梳理一下:

  1. OC 的 runtime 中使用了一个静态变量 SideTablesMap 用于存储所有对象的引用,它的类型是 StripedMap,为哈希表实现,其存储的 key 为对象指针,value 为 SideTable
  2. SideTable 本质上是一个冲突处理结构,其内部存储的是所有 hash 值相同的对象的引用列表,同时为了能够高效地在冲突中进行查找,SideTable 内部还是一个哈希表的结构,其存储的 key 为对象指针,value 为 weak_entry_t
  3. weak_entry_t 用于存储某一个对象的所有引用,其内部采用策略模式,使用了两个数据结构存储引用列表,首先是一个长度为 4 的数组,数组不够的话,就再创建一个哈希表存储所有引用

有了以上的信息之后,关于弱引用的一些概念就更容易理解了,

  1. 首先,弱引用本身是一个指针,它是指向对象的,所以在代码中直接调用弱引用的方法是完全可以的
  2. 当我们创建一个弱引用时,主要还是将这样一个弱引用关系存储在 SideTablesMap 中,简单来说就是找到对象对应的 weak_entry_t,将这个弱引用的指针(即指针的指针)放进去
  3. 当弱引用本身被销毁时,就是找到这个弱引用指向的对象对应的 weak_entry_t,在其中删掉这个弱引用指针,其对应的函数是 objc_destroyWeak
  4. 当对象本身被销毁时,就会找到这个对象的对应的 weak_entry_t,然后遍历它的所有弱引用指针,将弱引用置为 nil

弱引用的销毁

从运行时的汇编指令来看,当弱引用的生命周期结束时,会调用 objc_destroyWeak 来释放弱引用,其实现为:

void
objc_destroyWeak(id *location)
{
    (void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
        (location, nil);
}

本质上还是使用 storeWeak,但是这里只需要进行对旧指针的释放。

弱引用转强引用

一般当我们需要在某个代码块中使用弱引用的时候,通常会利用关键字__strong完成弱引用转强引用,例如:

- (void)wrTest2 {
    __strong typeof(self.weakObj) strongObj = self.weakObj;
    NSLog(@"");
}

通过查看运行时调用链,可以了解到这个过程主要就是调用 objc_loadWeakRetained 函数完成的,函数的实现约等于:

  1. 取出弱引用对应的对象
  2. 给 SideTable 上锁(上锁的目的应该是为了防止,在进行计数 +1 完成之前,进行了其他弱引用操作,使得弱引用指向的对象发生了变化,引起的状态问题)
  3. 调用对象的 retainWeakReference 完成计数 +1
  4. SideTable 解锁

对象的销毁

弱引用与普通指针的主要区别,就在于对象释放时,弱引用会同步地被置为 nil,这一功能的基础就是 SideTable 存储了对象的所有弱引用的指针,而这一操作的执行,就在对象销毁阶段。对象的 dealloc 实现为:

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);
    }
}

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());
}

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

在 object_dispose 的调用中,最终会调用到 clearDeallocating,在这里进行弱引用的清除,完成将对象的所有弱引用指针指向 nil 这一步,从而实现弱引用的功能。