【iOS】weak关键字的实现原理

前言

关于什么是weak关键字可以去看看我以前的一篇博客:【OC】 属性关键字

weak原理

1. SideTable

SideTable 这个结构体,前辈给它总结了一个很形象的名字叫引用计数和弱引用依赖表,因为它主要用于管理对象的引用计数和 weak 表。在 NSObject.mm 中声明其数据结构:

cpp 复制代码
struct SideTable {
// 保证原子操作的自旋锁
    spinlock_t slock;
    // 引用计数的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
    
    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void reset() { slock.reset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
}

slock是为了防止竞争选择的自旋锁

refcnts 是协助对象的 isa 指针的 extra_rc 共同引用计数的变量(对于对象结果,在后文提到)

接着我们来看一下SideTable中的这三个成员变量:

1.1 spinlock_t slock 自旋锁

自旋锁的效率高于互斥锁。但是我们要注意由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在哪里自旋,这就会浪费CPU时间。

在操作引用计数的时候对SideTable加锁,避免数据错误。

1.2 RefcountMap

cpp 复制代码
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

其中DenseMap 又是一个模板类:

objectivec 复制代码
template<typename KeyT, typename ValueT,
         bool ZeroValuesArePurgeable = false, 
         typename KeyInfoT = DenseMapInfo<KeyT> >
class DenseMap : public DenseMapBase<DenseMap<KeyT, ValueT, 
  ZeroValuesArePurgeable, KeyInfoT>, KeyT, ValueT, KeyInfoT, 
  ZeroValuesArePurgeable> {
  ...
  BucketT *Buckets;
  unsigned NumEntries;
  unsigned NumTombstones;
  unsigned NumBuckets;
  ...
}

比较重要的成员有这几个:

1.ZeroValuesArePurgeable 默认值是 false, 但 RefcountMap 指定其初始化为 true。 这个成员标记是否可以使用值为 0 (引用计数为 1) 的桶. 因为空桶存的初始值就是 0, 所以值为 0 的桶和空桶没什么区别.。如果允许使用值为 0 的桶, 查找桶时如果没有找到对象对应的桶, 也没有找到墓碑桶, 就会优先使用值为 0 的桶。

2.Buckets 指针管理一段连续内存空间, 也就是数组, 数组成员是 BucketT 类型的对象, 我们这里将 BucketT 对象称为桶(实际上这个数组才应该叫桶, 苹果把数组中的元素称为桶应该是为了形象一些, 而不是哈希桶中的桶的意思)。桶数组在申请空间后, 会进行初始化, 在所有位置上都放上空桶(桶的 key 为 EmptyKey 时是空桶), 之后对引用计数的操作, 都要依赖于桶。

桶的数据类型实际上是 std::pair, 类似于 swift 中的元祖类型, 就是将对象地址和对象的引用计数(这里的引用计数类似于isa, 也是使用其中的几个 bit 来保存引用计数, 留出几个 bit 来做其它标记位)组合成一个数据类型。

BucketT 的定义如下:

cpp 复制代码
typedef std::pair<KeyT, ValueT> BucketT;

3.NumEntries 记录数组中已使用的非空的桶的个数.

4.NumTombstones, Tombstone 直译为墓碑, 当一个对象的引用计数为0, 要从桶中取出时, 其所处的位置会被标记为 Tombstone. NumTombstones 就是数组中的墓碑的个数. 后面会介绍到墓碑的作用.

5.NumBuckets 桶的数量, 因为数组中始终都充满桶, 所以可以理解为数组大小.

cpp 复制代码
inline uint64_t NextPowerOf2(uint64_t A) {
    A |= (A >> 1);
    A |= (A >> 2);
    A |= (A >> 4);
    A |= (A >> 8);
    A |= (A >> 16);
    A |= (A >> 32);
    return A + 1;
}

这是对应 64 位的提供数组大小的方法, 需要为桶数组开辟空间时, 会由这个方法来决定数组大小. 这个算法可以做到把最高位的 1 覆盖到所有低位. 例如 A = 0b10000, (A >> 1) = 0b01000, 按位与就会得到 A = 0b11000, 这个时候 (A >> 2) = 0b00110, 按位与就会得到 A = 0b11110. 以此类推 A 的最高位的 1, 会一直覆盖到高 2 位、高 4 位、高 8 位, 直到最低位. 最后这个充满 1 的二进制数会再加 1, 得到一个 0b1000...(N 个 0). 也就是说, 桶数组的大小会是 2^n.

RefcountMap 的工作逻辑

1.通过计算对象地址的哈希值, 来从 SideTables 中获取对应的 SideTable. 哈希值重复的对象的引用计数存储在同一个 SideTable 里.

2.SideTable 使用 find() 方法和重载 \[\] 运算符的方式, 通过对象地址来确定对象对应的桶. 最终执行到的查找算法是 LookupBucketFor().

3.查找算法会先对桶的个数进行判断, 如果桶数为 0 则 return false 回上一级调用插入方法. 如果查找算法找到空桶或者墓碑桶, 同样 return false 回上一级调用插入算法, 不过会先记录下找到的桶. 如果找到了对象对应的桶, 只需要对其引用计数 + 1 或者 - 1. 如果引用计数为 0 需要销毁对象, 就将这个桶中的 key 设置为 TombstoneKey

cpp 复制代码
value_type& FindAndConstruct(const KeyT &Key) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))
      return *TheBucket;
    return *InsertIntoBucket(Key, ValueT(), TheBucket);
  }

4.插入算法会先查看可用量, 如果哈希表的可用量(墓碑桶+空桶的数量)小于 1/4, 则需要为表重新开辟更大的空间, 如果表中的空桶位置少于 1/8 (说明墓碑桶过多), 则需要清理表中的墓碑. 以上两种情况下哈希查找算法会很难查找正确位置, 甚至可能会产生死循环, 所以要先处理表, 处理表之后还会重新分配所有桶的位置, 之后重新查找当前对象的可用位置并插入. 如果没有发生以上两种情况, 就直接把新的对象的引用计数放入调用者提供的桶里.

墓碑的作用:

  1. 如果 c 对象销毁后将下标 2 的桶设置为空桶而不置为墓碑桶的话, 此时为 e 对象增加引用计数, 根据哈希算法查找到下标为 2 的桶时, 就会直接插入, 无法为已经在下标为 4 的桶中的 e 增加引用计数,但是我们正常的流程中c 对象销毁后下标 2的桶将会被置为墓碑桶,这样的话,在对e对象增加引用计数的时候,根据哈希算法找到下标为2的桶时,就会将2跳过,往后继续查找,直至找到e对象所对应的桶为止,或者直至找到空桶新建一个存e对象的桶
  2. 如果此时初始化了一个新的对象 f, 根据哈希算法查找到下标为 2 的桶时发现桶中放置了墓碑, 此时会记录下来下标 2. 接下来继续哈希算法查找位置, 查找到空桶时, 就证明表中没有对象 f, 此时 f 使用记录好的下标 2 的墓碑桶而不是查找到的空桶, 就可以利用到已经释放的位置,保证哈希表中前面部分都是被利用或者待利用的状态。

查找某对象对应桶的源码如下:

cpp 复制代码
bool LookupBucketFor(const LookupKeyT &Val,
                       const BucketT *&FoundBucket) const {
    ...
    if (NumBuckets == 0) { //桶数是0
      FoundBucket = 0;
      return false; //返回 false 回上层调用添加函数
    }
    ...
    unsigned BucketNo = getHashValue(Val) & (NumBuckets-1); //将哈希值与数组最大下标按位与
    unsigned ProbeAmt = 1; //哈希值重复的对象需要靠它来重新寻找位置
    while (1) {
      const BucketT *ThisBucket = BucketsPtr + BucketNo; //头指针 + 下标, 类似于数组取值
      //找到的桶中的 key 和对象地址相等, 则是找到
      if (KeyInfoT::isEqual(Val, ThisBucket->first)) {
        FoundBucket = ThisBucket;
        return true;
      }
      //找到的桶中的 key 是空桶占位符, 则表示可插入
      if (KeyInfoT::isEqual(ThisBucket->first, EmptyKey)) { 
        if (FoundTombstone) ThisBucket = FoundTombstone; //如果曾遇到墓碑, 则使用墓碑的位置
        FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
        return false; //找到空占位符, 则表明表中没有已经插入了该对象的桶
      }
      //如果找到了墓碑
      if (KeyInfoT::isEqual(ThisBucket->first, TombstoneKey) && !FoundTombstone)
        FoundTombstone = ThisBucket;  // 记录下墓碑
      //这里涉及到最初定义 typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap, 传入的第三个参数 true
      //这个参数代表是否可以清除 0 值, 也就是说这个参数为 true 并且没有墓碑的时候, 会记录下找到的 value 为 0 的桶
      if (ZeroValuesArePurgeable  && 
          ThisBucket->second == 0  &&  !FoundTombstone) 
        FoundTombstone = ThisBucket;

      //用于计数的 ProbeAmt 如果大于了数组容量, 就会抛出异常
      if (ProbeAmt > NumBuckets) {
          _objc_fatal("...");
      }
      BucketNo += ProbeAmt++; //本次哈希计算得出的下表不符合, 则利用 ProbeAmt 寻找下一个下标
      BucketNo&= (NumBuckets-1); //得到新的数字和数组下标最大值按位与
    }
  }

向某对象的引用计数桶插入代码如下:

objectivec 复制代码
BucketT *InsertIntoBucketImpl(const KeyT &Key, BucketT *TheBucket) {
    unsigned NewNumEntries = getNumEntries() + 1; //桶的使用量 +1
    unsigned NumBuckets = getNumBuckets(); //桶的总数
    if (NewNumEntries*4 >= NumBuckets*3) { //使用量超过 3/4
      this->grow(NumBuckets * 2); //数组大小 * 2做参数, grow 中会决定具体数值
      //grow 中会重新布置所有桶的位置, 所以将要插入的对象也要重新确定位置
      LookupBucketFor(Key, TheBucket);
      NumBuckets = getNumBuckets(); //获取最新的数组大小
    }
    //如果空桶数量少于 1/8, 哈希查找会很难定位到空桶的位置
    if (NumBuckets-(NewNumEntries+getNumTombstones()) <= NumBuckets/8) {
      //grow 以原大小重新开辟空间, 重新安排桶的位置并能清除墓碑
      this->grow(NumBuckets);
      LookupBucketFor(Key, TheBucket); //重新布局后将要插入的对象也要重新确定位置
    }
    assert(TheBucket);
    //找到的 BucketT 标记了 EmptyKey, 可以直接使用
    if (KeyInfoT::isEqual(TheBucket->first, getEmptyKey())) {
      incrementNumEntries(); //桶使用量 +1
    }
    else if (KeyInfoT::isEqual(TheBucket->first, getTombstoneKey())) { //如果找到的是墓碑
      incrementNumEntries(); //桶使用量 +1
      decrementNumTombstones(); //墓碑数量 -1
    }
    else if (ZeroValuesArePurgeable  &&  TheBucket->second == 0) { //找到的位置是 value 为 0 的位置
      TheBucket->second.~ValueT(); //测试中这句代码被直接跳过并没有执行, value 还是 0
    } else {
      // 其它情况, 并没有成员数量的变化(官方注释是 Updating an existing entry.)
    }
    return TheBucket;
  }

2. weak部分------weak_table_t

weak_table_t在SideTable结构体中,储存对象弱引用指针的Hash表,weak功能实现的核心数据结构

首先我们来看一下weak_table_t结构体的源码:

cpp 复制代码
struct weak_table_t {
    weak_entry_t *weak_entries;//连续地址空间的头指针,数组
    size_t    num_entries;//数组中已占用位置的个数
    uintptr_t mask;//数组下标最大值(即数组大小 -1)
    uintptr_t max_hash_displacement;//最大哈希偏移值
};

weak_table 是一个哈希表的结构, 根据 weak 指针指向的对象的地址计算哈希值, 哈希值相同的对象按照下标 +1 的形式向后查找可用位置, 是典型的闭散列算法. 最大哈希偏移值即是所有对象中计算出的哈希值和实际插入位置的最大偏移量, 在查找时可以作为循环的上限。

weak_table结构图:

2.1 weak_entry_t 的成员

cpp 复制代码
struct weak_entry_t {
    DisguisedPtr<objc_object> referent; //对象地址
    union {  //这里又是一个联合体, 苹果设计的数据结构的确很棒
        struct {
            // 因为这里要存储的又是一个 weak 指针数组, 所以苹果继续选择采用哈希算法
            weak_referrer_t *referrers; //指向 referent 对象的 weak 指针数组
            uintptr_t        out_of_line_ness : 2; //这里标记是否超过内联边界, 下面会提到
            uintptr_t        num_refs : PTR_MINUS_2; //数组中已占用的大小
            uintptr_t        mask; //数组下标最大值(数组大小 - 1)
            uintptr_t        max_hash_displacement; //最大哈希偏移值
        };
        struct {
            //这是一个取名叫内联引用的数组
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT]; //宏定义的值是 4
        };
    };
    // weak_entry_t 的赋值操作,直接使用 memcpy 函数拷贝 other 内存里面的内容到 this 中,
    // 而不是用复制构造函数什么的形式实现,应该也是为了提高效率考虑的...
    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    // 返回 true 表示使用 referrers 哈希数组 false 表示使用 inline_referrers 数组保存 weak_referrer_t
    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    // weak_entry_t 的构造函数
    
    // newReferent 是原始对象的指针,
    // newReferrer 则是指向 newReferent 的弱引用变量的指针。
    
    // 初始化列表 referent(newReferent) 会调用: DisguisedPtr(T* ptr) : value(disguise(ptr)) { } 构造函数,
    // 调用 disguise 函数把 newReferent 转化为一个整数赋值给 value。
    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        // 把 newReferrer 放在数组 0 位,也会调用 DisguisedPtr 构造函数,把 newReferrer 转化为整数保存
        inline_referrers[0] = newReferrer;
        // 循环把 inline_referrers 数组的剩余 3 位都置为 nil
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
}

我们通过对象的地址, 可以在 weak_table_t 中找到对应的 weak_entry_t, weak_entry_t 中保存了所有指向这个对象的 weak 指针

苹果在 weak_entry_t 中又使用了一个共用体, 第一个结构体中 out_of_line_ness 占用 2bit, num_refs 在 64 位环境下占用了 62bit, 所以实际上两个结构体都是 32 字节, 共用一段地址. 当指向这个对象的 weak 指针不超过 4 个, 则直接使用数组 inline_referrers, 省去了哈希操作的步骤, 如果 weak 指针个数超过了 4 个, 就要使用第一个结构体中的哈希表.

2.2 weak_table的大概逻辑

  • 在 ARC 下, 编译器会自动添加管理引用计数的代码, weak 指针赋值的时候, 编译器会调用 storeWeak 来赋值, 若 weak 指针有指向的对象, 那么会先调用 weak_unregister_no_lock() 方法来从原有的表中先删除这个 weak 指针, 然后再调用weak_register_no_lock() 来向对应的表中插入这个 weak 指针
  • 查找时先用被指向对象的地址来计算哈希值, 从 SideTables() 中找到对应的 SideTable, 再进一步使用这个对象地址来SideTable 的 weak_table 中找到对应的 weak_entry_t. 最终要进行操作的就是这个 weak_entry_t.
    如果这个对象的 weak 指针不超过 4 个, 则直接操作 inline_referrers 数组, 否则会为 referrers 数组申请内存, 采用哈希算法来管理表.
  • 删除旧的 weak 指针时, 会使用原本指向的对象的地址来查找对应的 weak_entry_t, 从中删除这个 weak 指针. 如果删除之后 weak 指针数组为空, 则销毁这个 weak_entry_t, 原有位置置空, 原本被指向对象的 isa 指针的 weak 引用标记位 0.
  • 添加新的 weak 指针时, 如果查找到对应的 weak_entry_t, 则将 weak 指针插入到 referrers 数组中. 如果没找到则创建一个 weak_entry_t 配置好后插入 weak_table_t 的数组中.

3. weak的重要实现方法

3.1 objc_initWeak 函数

objc_initWeak 函数的主要作用是根据传入的 newObj 对象初始化一个__weak修饰的对象指针,同时处理无效对象的情况,以及进行一些性能优化操作。

objectivec 复制代码
id objc_initWeak(id *location, id newObj) {
// 查看对象实例是否有效,无效对象直接导致指针释放
    if (!newObj) {
        *location = nil;
        return nil;
    }
    // 这里传递了三个 Bool 数值
    // 使用 template 进行常量参数传递是为了优化性能
    return storeWeakfalse/*old*/, true/*new*/, true/*crash*/>
    (location, (objc_object*)newObj);
}

然后我们看一下objc_initWeak()传入的两个参数代表什么:

  • location:__weak指针的地址,存储指针的地址,这样便可以在最后将其指向的对象置为nil。
  • newObj:所引用的对象。即例子中的p。

这个函数的功能如下:

  • 首先,它会检查传入的 newObj 对象是否有效,如果 newObj 是一个无效对象(即nil),那么它会将 location 指向的__weak指针设置为nil,并直接返回nil。
  • 如果 newObj 是一个有效对象,它会调用 storeWeak 函数,进行实际的弱引用初始化操作。
  • storeWeak 函数是底层的内部函数,它会将 newObj 对象存储到 location 指向的内存地址中,并设置标志位以及进行一些性能优化操作。

objc_initWeak函数有一个前提条件:就是object必须是一个没有被注册为__weak对象的有效指针。而value则可以是nil,或者指向一个有效的对象。

3.2 objc_storeWeak()

下面这段代码是Objective-C运行时中用于实现弱引用的 storeWeak 函数的模板实现。这个函数主要用于更新弱引用指针的指向,并处理多线程情况下的竞争冲突。

cpp 复制代码
// HaveOld:	 true - 变量有值
// 			false - 需要被及时清理,当前值可能为 nil
// HaveNew:	 true - 需要被分配的新值,当前值可能为 nil
// 			false - 不需要分配新值
// CrashIfDeallocating: true - 说明 newObj 已经释放或者 newObj 不支持弱引用,该过程需要暂停
// 			false - 用 nil 替代存储
template bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj) {
	// 该过程用来更新弱引用指针的指向
	// 初始化 previouslyInitializedClass 指针
    Class previouslyInitializedClass = nil;
    id oldObj;
    // 声明两个 SideTable
    // ① 新旧散列创建
    SideTable *oldTable;
    SideTable *newTable;
	// 获得新值和旧值的锁存位置(用地址作为唯一标示)
	// 通过地址来建立索引标志,防止桶重复
	// 下面指向的操作会改变旧值
  retry:
    if (HaveOld) {
    	// 更改指针,获得以 oldObj 为索引所存储的值地址
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (HaveNew) {
    	// 更改新值指针,获得以 newObj 为索引所存储的值地址
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
	// 加锁操作,防止多线程中竞争冲突
    SideTable::lockTwoHaveOld, HaveNew>(oldTable, newTable);
	// 避免线程冲突重处理
	// location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
    if (HaveOld  &&  *location != oldObj) {
        SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
        goto retry;
    }
    // 防止弱引用间死锁
    // 并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向
    if (HaveNew  &&  newObj) {
    	// 获得新对象的 isa 指针
        Class cls = newObj->getIsa();
        // 判断 isa 非空且已经初始化
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) {
        	// 解锁
            SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
            // 对其 isa 指针进行初始化
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));
            // 如果该类已经完成执行 +initialize 方法是最理想情况
            // 如果该类 +initialize 在线程中 
            // 例如 +initialize 正在调用 storeWeak 方法
            // 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记
            previouslyInitializedClass = cls;
			// 重新尝试
            goto retry;
        }
    }
    // ② 清除旧值
    if (HaveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }
    // ③ 分配新值
    if (HaveNew) {
        newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, 
                                                      (id)newObj, location, 
                                                      CrashIfDeallocating);
        // 如果弱引用被释放 weak_register_no_lock 方法返回 nil 
        // 在引用计数表中设置弱引用标记位
        if (newObj  &&  !newObj->isTaggedPointer()) {
        	// 弱引用位初始化操作
			// 引用计数那张散列表的weak引用对象的引用计数中标识为weak引用
            newObj->setWeaklyReferenced_nolock();
        }
        // 之前不要设置 location 对象,这里需要更改指针指向
        *location = (id)newObj;
    }
    else {
        // 没有新值,则无需更改
    }
    SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
    return (id)newObj;
}

我会逐步解释这个函数的主要步骤和作用:

  1. 首先,函数声明了一些变量,包括 previouslyInitializedClass 用于标记已经初始化过的类,oldObj 用于存储旧对象的引用,oldTable 和 newTable 用于表示旧对象和新对象的 SideTable(存储弱引用信息的数据结构)。
  2. 通过 retry 标签实现了一个重试的机制,用于处理线程冲突。
  3. 然后,函数根据传入的模板参数 HaveOld 和 HaveNew 来获取旧对象和新对象的 SideTable,并通过 SideTable::lockTwoHaveOld, HaveNew> 对这两个 SideTable 进行加锁操作,防止多线程竞争。
  4. 防止线程冲突:在加锁后,函数会检查 location 是否与 oldObj 一致,如果不一致,说明当前的 location 已经处理过 oldObj,但是又被其他线程所修改,为了避免冲突,需要重新执行 retry 标签处的代码,重新获取旧对象。
  5. 防止弱引用间死锁:函数会检查是否存在新对象并且新对象不为nil。如果是,则获得新对象的 isa 指针,并检查该 isa 是否已经初始化。如果没有初始化,则先对其进行初始化,并且设置 previouslyInitializedClass 作为标记,然后重新执行 retry 标签处的代码,以防止其他线程竞争。
  6. 接下来,函数会根据模板参数 HaveOld 来清除旧值(取消旧对象的弱引用),并根据模板参数 HaveNew 来分配新值(添加新对象的弱引用)。
  7. 如果新对象被成功注册并分配了弱引用,则将弱引用位进行初始化,并将 location 指向的对象指针更新为新对象的指针。
  8. 最后,函数通过 SideTable::unlockTwoHaveOld, HaveNew> 对加锁的两个 SideTable 进行解锁,并返回新对象的指针。

3.3 weak_register_no_lock

cpp 复制代码
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    // 如果referent为nil 或 referent 采用了TaggedPointer计数方式,直接返回,不做任何操作
    if (!referent  ||  referent->isTaggedPointer()) return referent_id;

    // 确保被引用的对象可用(没有在析构,同时应该支持weak引用)
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }
    // 正在析构的对象,不能够被弱引用
    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }

    // now remember it and where it is being stored
    // 在 weak_table中找到referent对应的weak_entry,并将referrer加入到weak_entry中
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) { // 如果能找到weak_entry,则讲referrer插入到weak_entry中
        append_referrer(entry, referrer); 	// 将referrer插入到weak_entry_t的引用数组中
    } 
    else { // 如果找不到,就新建一个
        weak_entry_t new_entry(referent, referrer);  
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}

这段代码是Objective-C运行时中用于在弱引用表中注册一个弱引用的函数 weak_register_no_lock 的实现。该函数用于向 weak_table 中添加一个弱引用关系,记录一个对象的弱引用指针。

现在,我将逐步解释代码的主要步骤和作用:

  1. 首先,函数将传入的 referent_id 和 referrer_id 分别转换为 objc_object 类型的指针,分别赋值给 referent 和 referrer 变量。
  2. 然后,函数会检查 referent 是否为nil或采用了TaggedPointer计数方式(Tagged Pointer是一种优化机制,用于在一些情况下直接将对象指针存储在指针本身,而不通过额外的内存分配,这里不需要处理弱引用)。
  3. 接下来,函数会检查被引用的对象 referent 是否可用,即它没有在析构过程中,并且支持弱引用。这里需要注意,在Objective-C中,有些对象可能会通过重写allowsWeakReference方法来决定是否支持弱引用。所以,对于有自定义引用计数方式的对象,函数会调用allowsWeakReference方法来检查对象是否支持弱引用。
  4. 如果被引用的对象 referent 正在析构过程中(deallocating为true),那么根据 crashIfDeallocating 参数的值,函数会决定是返回nil还是抛出异常。如果 crashIfDeallocating 为true,则会通过 _objc_fatal 抛出异常,否则返回nil。
  5. 如果被引用的对象 referent 可用,并且支持弱引用,则继续执行下面的步骤。
  6. 函数会在 weak_table 中查找是否已经存在 referent 对应的弱引用条目 weak_entry。如果找到了,就将 referrer 加入到该 weak_entry 中的引用数组中。如果找不到,则创建一个新的 weak_entry 并将其插入到 weak_table 中,然后将 referrer 加入到该新的 weak_entry 中的引用数组中。
  7. 最后,函数返回 referent_id,即传入的被引用对象 referent 的指针。

3.4 weak_entry_for_referent

cpp 复制代码
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;  // 这里通过 & weak_table->mask的位操作,来确保index不会越界
    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); // 触发bad weak table crash
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) { // 当hash冲突超过了可能的max hash 冲突时,说明元素没有在hash表中,返回nil 
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

这是一个名为 weak_entry_for_referent 的函数,用于在弱引用表中查找给定被引用对象 referent 对应的 weak_entry 条目。

3.5 append_referrer

cpp 复制代码
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    if (! entry->out_of_line()) { // 如果weak_entry 尚未使用动态数组,走这里
        // Try to insert inline.
        //尝试插入内联引用的数组
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }
        
        // 如果inline_referrers的位置已经存满了,则要转型为referrers,做动态数组。
        // Couldn't insert inline. Allocate out of line.
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
        // This constructed table is invalid, but grow_refs_and_insert
        // will fix it and rehash it.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[I];
        }
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    // 对于动态数组的附加处理:
    assert(entry->out_of_line()); // 断言: 此时一定使用的动态数组

    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) { // 如果动态数组中元素个数大于或等于数组位置总空间的3/4,则扩展数组空间为当前长度的一倍
        return grow_refs_and_insert(entry, new_referrer); // 扩容,并插入
    }
    
    // 如果不需要扩容,直接插入到weak_entry中
    // 注意,weak_entry是一个哈希表,key:w_hash_pointer(new_referrer) value: new_referrer
    
    // 细心的人可能注意到了,这里weak_entry_t 的hash算法和 weak_table_t的hash算法是一样的,同时扩容/减容的算法也是一样的
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask); // '& (entry->mask)' 确保了 begin的位置只能大于或等于 数组的长度
    size_t index = begin;  // 初始的hash index
    size_t hash_displacement = 0;  // 用于记录hash冲突的次数,也就是hash再位移的次数
    while (entry->referrers[index] != nil) {
        hash_displacement++;
        index = (index+1) & entry->mask;  // index + 1, 移到下一个位置,再试一次能否插入。(这里要考虑到entry->mask取值,一定是:0x111, 0x1111, 0x11111, ... ,因为数组每次都是*2增长,即8, 16, 32,对应动态数组空间长度-1的mask,也就是前面的取值。)
        if (index == begin) bad_weak_table(entry); // index == begin 意味着数组绕了一圈都没有找到合适位置,这时候一定是出了什么问题。
    }
    if (hash_displacement > entry->max_hash_displacement) { // 记录最大的hash冲突次数, max_hash_displacement意味着: 我们尝试至多max_hash_displacement次,肯定能够找到object对应的hash位置
        entry->max_hash_displacement = hash_displacement;
    }
    // 将ref存入hash数组,同时,更新元素个数num_refs
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}

这段代码首先确定是使用定长数组还是动态数组,如果是使用定长数组,则直接将weak指针地址添加到数组即可,如果定长数组已经用尽,则需要将定长数组中的元素转存到动态数组中。

接着我们来看一下weak指针移除弱引用,需要清除weak_entry时调用的方法:weak_unregister_no_lock,方法里面将旧的weak指针地址移除了。

3.6 weak_unregister_no_lock

cpp 复制代码
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id)
{
	//对象的地址
    objc_object *referent = (objc_object *)referent_id;
    //weak指针地址
    objc_object **referrer = (objc_object **)referrer_id;

    weak_entry_t *entry;

    if (!referent) return;

    if ((entry = weak_entry_for_referent(weak_table, referent))) { // 查找到referent所对应的weak_entry_t
        remove_referrer(entry, referrer);  // 在referent所对应的weak_entry_t的hash数组中,移除referrer
       
        // 移除元素之后, 要检查一下weak_entry_t的hash数组是否已经空了
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break;
                }
            }
        }

        if (empty) { // 如果weak_entry_t的hash数组已经空了,则需要将weak_entry_t从weak_table中移除
            weak_entry_remove(weak_table, entry);
        }
    }
    // Do not set *referrer = nil. objc_storeWeak() requires that the 
    // value not change.
}

大概流程:

首先,它会在weak_table中找出referent对应的weak_entry_t

在weak_entry_t中移除referrer

移除元素后,判断此时weak_entry_t中是否还有元素 (empty==true?)

如果此时weak_entry_t已经没有元素了,则需要将weak_entry_t从weak_table中移除

4. dealloc

4.1 rootDealloc

当对象的引用计数为0时,底层会调用_objc_rootDealloc方法对对象进行释放,而在_objc_rootDealloc方法里面会调用rootDealloc方法。如下是rootDealloc方法的代码实现:

cpp 复制代码
xinline 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);
    }
}

大概流程:

  • 首先判断对象是否是Tagged Pointer,如果是则直接返回。
  • 如果对象是采用了优化的isa计数方式,且同时满足对象没有被weak引用!isa.weakly_referenced、没有关联对象!isa.has_assoc、没有自定义的C++析构方法!isa.has_cxx_dtor、没有用到SideTable来引用计数!isa.has_sidetable_rc则直接快速释放。
  • 如果不能满足2中的条件,则会调用object_dispose 方法。

4.2 object_dispose

cpp 复制代码
void *objc_destructInstance(id obj)
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_associations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }

    return obj;
}

如果有自定义的C++析构方法,则调用C++析构函数。如果有关联对象,则移除关联对象并将其自身从Association Manager的map中移除。调用clearDeallocating 方法清除对象的相关引用。

4.3 clearDeallocating

cpp 复制代码
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());
}

clearDeallocating中有两个分支,先判断对象是否采用了优化isa引用计数,如果没有的话则需要调用sidetable_clearDeallocating方法清理对象存储在SideTable中的引用计数数据。如果对象采用了优化isa引用计数,则判断是否有使用SideTable的辅助引用计数(isa.has_sidetable_rc)或者有weak引用(isa.weakly_referenced),符合这两种情况中一种的,调用clearDeallocating_slow 方法。

4.4 sidetable_clearDeallocating

cpp 复制代码
void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    //清除所有弱表项
	//清除额外的保留计数和释放位
	//(如果额外保留计数==0,则修复警告或中止)
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        table.refcnts.erase(it);
    }
    table.unlock();
}

4.5 clearDeallocating_slow

cpp 复制代码
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this]; // 在全局的SideTables中,以this指针为key,找到对应的SideTable
    table.lock();
    if (isa.weakly_referenced) { // 如果obj被弱引用
        weak_clear_no_lock(&table.weak_table, (id)this); // 在SideTable的weak_table中对this进行清理工作
    }
    if (isa.has_sidetable_rc) { // 如果采用了SideTable做引用计数
        table.refcnts.erase(this); // 在SideTable的引用计数中移除this
    }
    table.unlock();
}

4.6 weak_clear_no_lock

cpp 复制代码
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); // 找到referent在weak_table中对应的weak_entry_t
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    // 找出weak引用referent的weak 指针地址数组以及数组长度
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i]; // 取出每个weak ptr的地址
        if (referrer) {
            if (*referrer == referent) { // 如果weak ptr确实weak引用了referent,则将weak ptr设置为nil,这也就是为什么weak 指针会自动设置为nil的原因
                *referrer = nil;
            }
            else if (*referrer) { // 如果所存储的weak ptr没有weak 引用referent,这可能是由于runtime代码的逻辑错误引起的,报错
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry); // 由于referent要被释放了,因此referent的weak_entry_t也要移除出weak_table
}

最后再来看一下weak指针销毁的方法:

cpp 复制代码
void
objc_destroyWeak(id *location)
{
    (void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
        (location, nil);
}

该处调用storeWeak方法之后,由于没有指向新的对象,若我们的weak指针原来已经指向一个对象的话就会到:weak_unregister_no_lock中来将旧的weak指针地址移除掉置为nil。

总结

  • weak的原理在于底层维护了一张weak_table_t结构的hash表,key是所指对象的地址,value是weak指针的地址数组。
  • weak 关键字的作用是弱引用,所引用对象的计数器不会加1,并在引用对象被释放的时候自动被设置为 nil。
  • 对象释放时,调用clearDeallocating函数根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
  • 文章中介绍了SideTable、weak_table_t、weak_entry_t这样三个结构。
相关推荐
茶底世界之下5 小时前
类型擦除之后,Metal 滤镜组合为什么不能只执行一个 Filter?
ios·swift
传奇开心果编程7 小时前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
tink15 小时前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
黑科技iOS上架18 小时前
iOS隐私合规扫描工具
ios·审核
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程18 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer