OC归纳总结 -- (6)深入理解weak弱引用

2,241 阅读21分钟

weak指针, 也就是弱引用是OC中非常重要的概念, 相对于OC对象的alloc, retain,release, dealloc等内存管理的知识点, 弱引用weak指针的理解更加困难, 因此这里专门通过runtime的源码分析来归纳总结一下OC中弱引用相关的内容

先贴一张图:

image.png

1. weak指针(弱引用)的生命周期函数

objc-runtime的源码中直接copy如下代码, 包括以下内容, 注意方法中两个参数分别是弱引用指针的地址, 弱引用指向的oc对象:

  1. 弱引用weak的初始化objc_initWeak
  2. 弱引用weak赋值objc_storeWeak
  3. 弱引用weak的释放objc_destroyWeak
  4. 以及使用弱引用指针使用的objc_loadWeak

下面解释了两种常见的场景中初始化弱引用的方法, 两种场景分别是:

// 第一种场景, 初始化一个指向nil的弱引用指针
__weak id weakPtr;

// 第二种场景, 初始化一个指向 non-nil 的弱引用指针
NSObject *o = [NSObject new];
__weak id weakPtr = o;

他们都会触发以下方法, 其中

  1. 第一个参数是弱引用指针的地址!!! 注意一定是地址
  2. 第二个参数是弱引用指针要指向的OC对象!!!
/*
参数1: location Address of __weak ptr. 
参数2:  newObj Object ptr. 
*/
id objc_initWeak(id *location, id newObj);

该方法的具体代码逻辑只用了两步, 源码中有本人的注释:

  1. 检查赋值或者初始化给弱引用指针的OC对象是否是nil, 如果是nil, 弱引用指针直接指向nil.
  2. 否则, 直接调用 storeWeak方法, 这是一个c++模板方法, 因此这里是初始化一个弱引用指针, 那么模板参数用的DontHaveOld, DoHaveNew, DoCrashIfDeallocating
/** 
 * Initialize a fresh weak pointer to some object location. 
 * It would be used for code like: 
 *
 * (The nil case) 
 * __weak id weakPtr;
 * (The non-nil case) 
 * NSObject *o = ...;
 * __weak id weakPtr = o;
 * 
 * This function IS NOT thread-safe with respect to concurrent 
 * modifications to the weak variable. (Concurrent weak clear is safe.)
 *
 * @param location Address of __weak ptr. 
 * @param newObj Object ptr. 
 */
id objc_initWeak(id *location, id newObj) {
		// 先判断弱引用指针指向的OC对象是否是空的, 如果是nil
		// 那么弱引用指针指向的值是 nil!!!
		// 此时本方法返回 nil
    if (!newObj) {
        *location = nil;
        return nil;
    }

    // location 是 __weak ptr 也就是弱引用指针的地址!!!
    // 强制将 newObj 转化成 objc_object * 对象
    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

// 修改弱引用指针指向, 会指向一个新的oc对象
id objc_storeWeak(id *location, id newObj) {
    return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object *)newObj);
}

// 弱引用指针清理方法, 也即
// __weak NSObject *weakObj = oc 对象;
// weakObj = nil;
void objc_destroyWeak(id *location) {
    // 释放以后才会调用 objc_destoryWeak 方法
    (void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
        (location, nil);
}

上面的几个方法, 最终会触发一个c++的模板方法storeWeak<...>, 该模板方法本质是更新一个弱引用指针本身的值,其中模板参数有如下几个:

  1. HaveOld, 表示弱引用指针当前是否指向了一个旧OC对象, 称为old object, 如果有, 那么后续需要先处理old object的弱引用表中的的本弱引用指针的信息
  2. HaveNew 表示弱引用指针是否将要指向一个新的OC对象, 称为 new object
  3. DoCrashIfDeallocating 表示, 如果弱引用指针将要指向的new object 正在deallocating, 那么会crash!!! 这里简单解释就是下面这种写法会crash, 因此在调用对象的dealloc方法时, 该OC对象的isa.deallocating == true .
-(void)dealloc{
	__weak typeof(self) weakself = self;
}

关键的更新弱引用变量的方法如下:


// Update a weak variable.
// If HaveOld is true, the variable has an existing value 
//   that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be 
//   assigned into the variable. This value might be nil.
// If CrashIfDeallocating is true, the process is halted if newObj is 
//   deallocating or newObj's class does not support weak references. 
//   If CrashIfDeallocating is false, nil is stored instead.
enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template <HaveOld haveOld, HaveNew haveNew,
          enum CrashIfDeallocating crashIfDeallocating>
static id 
storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);

    // 需要先获取 old new oc对象的弱引用表
    // oc对象 -> oc对象的sidetable -> weak_table -> weak_entry [oc对象, 弱引用指针地址]
    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;
    }

    // 如果 new oc 对象还没有初始化, 先初始化
    ...
    // old new oc对象的sidetable 都上锁
    
    // 先从 old oc 对象的 weak_table 中删除与本弱引用指针关联的weak_entry_t
    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // 然后, 用[new oc对象, 弱引用指针的地址]构建 weak_entry_t, 添加到new oc对象的sidetable的weak_table中
    // 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

        // 如果 new oc对象不是 taggedPointer 对象
        // 对这个对象的 isa中的 weaklyReference 标志位设置一下
        // Set is-weakly-referenced bit in refcount table.
        if (!newObj->isTaggedPointerOrNil()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // 将弱引用指针指向 new oc 对象!!!
        // 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);

    // 强制设置一下 new obj 有一个弱引用指针指向的 weaklyReference 标志位
    // 这个标志位标记以后, 在 new obj 对象释放的时, 通过该标志位判断是否有弱引用指针指向new obj, 如果有, 需要处理 weak_table
    
    // 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);

    // 返回的指针, 弱引用指针指向的oc对象!!!
    return (id)newObj;
}

其实还有一种场景, block 中使用strongA = weakB的场景, 或者使用弱引用指针的地方, 会调用如下方法:

/** 
 * This loads the object referenced by a weak pointer and returns it, after
 * retaining and autoreleasing the object to ensure that it stays alive
 * long enough for the caller to use it. This function would be used
 * anywhere a __weak variable is used in an expression.
 * 
 * @param location The weak pointer address
 * 
 * @return The object pointed to by \e location, or \c nil if \e location is \c nil.
 */
id objc_loadWeak(id *location) {
	  // 弱引用指针指向的oc对象是 nil了. 直接返回 nil
    if (!*location) return nil;
  	// 否则弱引用指向的oc对象, 然后添加到 auto release pool 并返回回去!
    return objc_autorelease(objc_loadWeakRetained(location));
}

从以上代码中能看到, 实际上oc中的弱引用指针/强引用指针与C++的指针最大的区别在于对oc对象的内存管理方面的处理!

除此以外, 弱引用/强引用指针与C++的普通指针并没有本质区别, 都是指向一个堆区的地址空间!!!

当我们使用oc的弱引用/强引用指针时, 编译器和runtime在底层悄悄维护着oc对象的引用计数信息和弱引用表!!!

上面内容中关键的方法如下:

  1. sideTable相关的API
  2. weak_unregister_no_lock
  3. weak_register_no_lock

2. sideTable 的结构

首先, 在上述的源码中, 能看到获取一个oc对象的sidetable都是通过如下方法:

SideTable *table = &SideTables()[obj];

而其中关键的就是:

这里面涉及SideTables 这个全局的静态方法:

// runtime 中为了 SideTablesMap 这个静态对象能快速初始化,使用ExplicitInit类模板
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

// 可以简单理解成全局 static 的 StripedMap<SideTable> 对象
static StripedMap<SideTable>& SideTables() {
		// get() 方法是 ExplicitInit模板中的方
    return SideTablesMap.get();
}

// StripedMap<T> is a map of void* -> T, sized appropriately 
// for cache-friendly lock striping. 
// For example, this may be used as StripedMap<spinlock_t>
// or as StripedMap<SomeStruct> where SomeStruct stores a spin lock.
template<typename T>
class StripedMap {
    enum { CacheLineSize = 64 };

    struct PaddedT {
        T value alignas(CacheLineSize);
    };
    enum { StripeCount = 8 };
    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
 public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }
    constexpr StripedMap() {}
};

从上述的源码中很清楚看清:

从源码中看出:

  1. StripedMap<T> 是一个类模板, 它本质是是一个散列表!!! 底层的数据容器使用的内存空间连续的array, 并且它含有8个元素, 使用的hash()散列函数依赖一个const void *指针, 底层会直接拿到指针指向的地址作为散列函数计算基础, 添加一些算数运算得到index!!!
  2. StripedMap<T>重写下表运算符[], 只要传入oc对象的地址, 就能通过散列函数, 计算具体的关联的T对象
  3. runtime使用一个全局的静态的 StripedMap<SideTable>的对象, 来管理所有oc对象的SideTable, 只要oc对象的地址通过散列函数计算出数组的index, 就能获取oc对象对应的sideTable

runtime 中很多获取objc_object对象的sidetable, 使用如下方法:

SideTable& table = SideTables()[this];

全局SideTables散列表相关小结:

  1. 数据结构: sideTables的底层数据结构是HashTable散列表
  2. 底层容器: 在静态数据区, 拥有8个元素的不可变array数组, 成员是SideTable
  3. 散列函数: 使用objc_object*对象的地址结合一定算法作为散列函数, 计算index
  4. KV信息:key是上面计算的index, value 是oc对象的 sideTable
  5. 散列冲突: 表示不同的oc对象会共用 SideTable!!!
  6. Hash扩展因子 : 无(固定长度数组)

对于SideTable的结构体:

struct SideTable {
    spinlock_t slock; // 自旋锁, 用来保护 refcnts 和 weak_table 的细粒度锁
    RefcountMap refcnts; // 额外的引用计数表 -- 散列Map
    weak_table_t weak_table; // 弱引用表成员 -- 散列表
    
    ... // 一些c++ 构造函数, 析构, 锁方法等
}

简单点说前面runtime源码中涉及到的弱引用, 操作的都是sideTable中的weak_table表, 简单来说就是注册与移除, 这里都会涉及到弱引用表的结构!!!

// 从oc对象的弱引用表中注册弱引用指针的地址信息
weak_register_no_lock(&newTable->weak_table, (id)newObj, location, CrashIfDeallocating);
// 从oc对象的弱引用表中移除弱引用指针的地址信息
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);

3. weak_table_t 弱引用表的结构

从前文看到弱引用weak指针的操作, 本质上runtime会在底层对weak指针指向的oc对象的弱引用表和isa进行一系列的操作, 在了解这些操作时, 必须对 weak_table的数据结构有一个清晰的认识.

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries; 				// 存储的实际的 key-value 键值对, 底层容器是可变array 
    size_t    		num_entries;      		// 当前存储的实体数量
    uintptr_t 		mask;								  // 散列表的最大容量
    uintptr_t 		max_hash_displacement;//当前最大散列碰撞次数
};

// 弱引用表中实际存储的 key-value 信息
// kv内容简写成:  (object, weak pointer)
struct weak_entry_t {
  // key --> oc对象指针
  DisguisedPtr<objc_object> referent;
  
  // value -> 用一个数组, 存储 weak pointer, 也就是弱引用指针!!!
  // 使用 union 联合体节省内存
  union {
    struct {
      weak_referrer_t *referrers; // HashTable 存储弱引用指针的可变数组!!!
      uintptr_t        out_of_line_ness : 2; // 判断使用的固定数组 还是 HashTable
      uintptr_t        num_refs : PTR_MINUS_2;
      uintptr_t        mask;
      uintptr_t        max_hash_displacement;
    };
    
    struct {
      // out_of_line_ness field is low bits of inline_referrers[1]
      weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT]; // 存储弱引用指针的固定数组
    };
  }; 
  
  ...  // 其他方法
};

// The address of a __weak variable. --> 表示 __weak 变量的地址!!! 弱引用指针的地址
// 这里用混淆方式, 为了减少对 memory analysis tools 的影响
typedef DisguisedPtr<objc_object *> weak_referrer_t;

weak_table总结如下:

  1. 数据结构: weak_table_t的底层数据结构是HashTable散列表,它会被sideTablespinlock进行保护
  2. 底层容器: 分配在堆上可变的循环array数组, 每个成员是weak_entry_t结构体
  3. 散列函数: 依赖weak_entry_treferent, 也就是oc对象计算出来的一个index!!!
  4. KV信息: key是上面计算的index, value 是oc对象weak_entry_t 结构体!!!
  5. 散列冲突: 当发生散列冲突时, 类似于开放寻址法, 查找array的下一个空的位置存储weak_entry_t
  6. Hash扩展因子 : HashTable初始容量是64, 增加到0.75时, 扩容成原来的2倍; 当减少时, 1/16时, 减少到原来1/2

其中weak_table的散列函数:

size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);

weak_table的散列冲突时的处理:

size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask); // 散列函数
size_t index = begin; 
size_t hash_displacement = 0;
// 注意这里是 weak_table->weak_entries!!! 不要和后面弄混淆
// 从array的 index 位置开始, 向后找第一个空的位置!!! (循环数组)
while (weak_entries[index].referent != nil) {
  index = (index+1) & weak_table->mask; // 循环查找!!!
  if (index == begin)
  	bad_weak_table(weak_entries); // 一个空的没有, 直接crash!
  hash_displacement++; // 记录散列冲突的最大次数
}

另外一个比较复杂的是 weak_entry_t 的内部的弱引用指针存储相关内容, runtime使用了一个union来优化内存. 当指向oc对象的弱引用指针个数为4个以内时, 使用固定长度array存储weak pointer, 超过以后使用HashTable(这里我们在后面解释)!!!

weak_entry_t总结如下:

  1. 数据结构: weak_entry_t的是一对Pair!!!
  2. Pair的结构: (oc对象: weak pointers), 并且指针都是混淆过的!!!
  3. weak pointers 使用联合体实现,
    1. 如果weak pointers数量不大于4, 那么使用定长数组存储弱引用指针
    2. 如果数量>4, 那么会使用HashTable存储

weak pointers的数量超过4个时, runtime会使用HashTable来存储!!! 它的信息如下:

  1. 数据结构是HashTable散列表
  2. 底层容器: 分配在堆上可变的循环array数组, 每个成员都是weak pointer的混淆结果
  3. 散列函数: 依赖weak pointer的地址!! 也就是new_referrer计算出来的一个index!!!
  4. KV信息: key是上面计算的index, value是指向oc对象weak pointer指针!
  5. 散列冲突: 当发生散列冲突时, 类似于开放寻址法, 查找array的下一个空的位置存储weak pointer
  6. Hash扩展因子 : HashTable初始容量是8, 增加到0.75时, 扩容成原来的2倍

注意: 上面所有的weak pointer 都是混淆结果的代称

其中存储weak pointers的散列函数, :

//new_referrer 是 weak pointer 的地址
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);

weak_entry的散列冲突时的处理:

 // new_referrer 是新的 __weak NSObject *xx;  这个弱引用指针的 xx 的地址
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
		// 这里注意, 查询的是 referrers 数组!!! 不要和前面 weak_table 弄混
		// 从array的 index 位置开始, 向后找第一个空的位置!!! (循环数组)
    while (entry->referrers[index] != nil) {
        hash_displacement++;
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
    }
    if (hash_displacement > entry->max_hash_displacement) {
        entry->max_hash_displacement = hash_displacement;
    }

后面会仔细拆分弱引用表HashTable的增加, 删除和清理方法

  1. weak_register_no_lock : 向弱引用表插入(object, weak pointer) weak_entry_t结构体
  2. weak_unregister_no_lock: 从弱引用表移除(object, weak pointer) weak_entry_t结构体
  3. weak_clear_no_lock: 在oc对象析构的时候调用, 设置所有的weak pointer为nil

4. weak_table_t 弱引用表的增加操作

创建一个新的__weak NSObject*指向对象, 通过前面的文章, 流程如下:

  1. objc_initWeak
  2. objc_storeWeak
  3. weak_register_no_lock
  4. 此时拥有. new oc 对象, 以及 weak pointer 的地址!!
  5. 构造 (oc对象, weak pointer)的 weak_entry_t , 添加到 weak_table_t
/** 
 * Registers a new (object, weak pointer) pair. Creates a new weak
 * object entry if it does not exist.
 * 
 * @param weak_table The global weak table.
 * @param referent The object pointed to by the weak reference.
 * @param referrer The weak pointer address.
 */
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions) {
    objc_object *referent = (objc_object *)referent_id; // oc 对象
    objc_object **referrer = (objc_object **)referrer_id; // weak pointer
    // 如果是 taggedPointer , 不受 weak pointer 影响
    if (referent->isTaggedPointerOrNil()) 
    		return referent_id;

    // 如果 oc 对象正在 dealloc, 此时会crash
 		... 

    // now remember it and where it is being stored
    // 两种情况:
    // 1. 如果 oc 对象之前已经有 weak pointer 指向, 那么添加到该 weak_entry 中
    // 2. 如果没weak pointer, 那么新建一个(oc对象, weak pointer)的weak_entry, 插入到weak_table中
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);
    } else {
        weak_entry_t new_entry(referent, referrer);
        // 如果需要新插入 weak_entry_t, 需要判断 hashtable 是否需要增长
        weak_grow_maybe(weak_table);  
        weak_entry_insert(weak_table, &new_entry);
    }
    
    return referent_id;
}

很常见的两种情况, 两条路的走法不一样:

NSObject *obj = [NSObject new];
__weak NSObject* weakObj1 = obj; // 情况1
__weak NSObject* weakObj2 = obj; // 情况2
  1. 如果当前__weak NSObject* 指向的oc对象没有弱引用指针指向, 可能需要对weak_table进行扩表, 然后将新的weak_entry插入weak_table中.
  2. 如果当前__weak NSObject* 指向的oc对象之前有弱引用指针指向, 此时它的weak_table中的weak_entry_t肯定存在. 因此需要, 先找到weak_entry_t, 再讲weak pointer 使用append_referrer加入进去

先分析第一种, 插入新的weak_entry_tweak_table

这种情况也就是:

__weak NSObject* weakObj = [NSObject new];

具体的代码如下, 内部保留着注释:

// 表长度, 就是 mask
#define TABLE_SIZE(weak_table) (weak_table->mask ? weak_table->mask + 1 : 0)

// Grow the given zone's table of weak references if it is full.
static void weak_grow_maybe(weak_table_t *weak_table) {
		// 获取旧值
    size_t old_size = TABLE_SIZE(weak_table);

		// HashTable 的扩张因子阈值是 0.75
    if (weak_table->num_entries >= old_size * 3 / 4) {
        // 扩张的大小是 *2, 初始时是 64!!!!
        weak_resize(weak_table, old_size ? old_size * 2 : 64);
    }
}

// 扩表操作
static void weak_resize(weak_table_t *weak_table, size_t new_size) {
    size_t old_size = TABLE_SIZE(weak_table);
    // 旧数据
    weak_entry_t *old_entries = weak_table->weak_entries;
    // 重新分配 new_size 的堆空间
    weak_entry_t *new_entries = (weak_entry_t *)
        calloc(new_size, sizeof(weak_entry_t));

    weak_table->mask = new_size - 1;
    weak_table->weak_entries = new_entries;
    weak_table->max_hash_displacement = 0;
    weak_table->num_entries = 0;  // restored by weak_entry_insert below
    // 将旧的 weak_entry 结构体, 一个个插入新的空间
    if (old_entries) {
        weak_entry_t *entry;
        weak_entry_t *end = old_entries + old_size;
        for (entry = old_entries; entry < end; entry++) {
            // 注意 resize的时, 如果 weak_entry中的 oc对象不存在(可能被释放了), 那么不插入到新的
            if (entry->referent) {
                weak_entry_insert(weak_table, entry);
            }
        }
        free(old_entries);
    }
}

/** 
 * Add new_entry to the object's table of weak references.
 * Does not check whether the referent is already in the table.
 */
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry) {
    weak_entry_t *weak_entries = weak_table->weak_entries;
    ASSERT(weak_entries != nil);

		// 使用 散列函数!!! 计算循环可变数组array 的 index!!!
		// 散列函数使用的是 oc对象的地址计算的!!!
    size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_entries[index].referent != nil) {
        index = (index+1) & weak_table->mask;
        if (index == begin)
            bad_weak_table(weak_entries);
        hash_displacement++; // 散列冲突了. 继续找空位
    }

		// 正式插入!!!
    weak_entries[index] = *new_entry; // 使用拷贝构造函数!!!
    // 一共插入的 weak_entry_t 的数量, 使用 weak_table->num_entries 存储
    weak_table->num_entries++;
    // 更新散列函数冲突最大出力次数
    if (hash_displacement > weak_table->max_hash_displacement) {
        weak_table->max_hash_displacement = hash_displacement;
    }
}

从以上代码中能看到, 如果是完全初始化的weak_table第一次插入weak_entry_t, 那么会构造这个HashTable, 在底层构造初始长度为64的可变长的循环array作为散列表的容器. 其他weak_table散列表的内容上文以及总结了.

再分析第二种, 插入的weak_entry_t 中的oc对象之前已经有弱引用指指向了

情况二的内容:

NSObject *obj = [NSObject new];
__weak NSObject* weakObj1 = obj; // 情况1
__weak NSObject* weakObj2 = obj; // 情况2

关键代码如下:

weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
		append_referrer(entry, referrer);
}

weak_entry_for_referent 是为了找到oc对象weak_entry_t源码如下, 代码比较简单, 直接参考注释:

/** 
 * Return the weak reference table entry for the given referent. 
 * If there is no entry for referent, return NULL. 
 * Performs a lookup.
 *
 * @param weak_table 
 * @param referent The object. Must not be nil.
 * 
 * @return The table of weak referrers to this object. 
 */
static 
weak_entry_t* weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent) {
    ASSERT(referent);
		// 获取HashTable
    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

  	// 使用Hash函数, 获取 index
    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    // 将 index 结合判断是否发生 hash碰撞, 找到查询的 index
    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;
        }
    }
  	// 返回 oc对象关联的  weak_entry!!!
    return &weak_table->weak_entries[index];
}

比较复杂的是给weak_entry_t 新增一个新的weak pointer, 核心函数是append_referrer:

// The address of a __weak variable.
// These pointers are stored disguised so memory analysis tools
// don't see lots of interior pointers from the weak table into objects.
typedef DisguisedPtr<objc_object *> weak_referrer_t; // 


/** 
 * Add the given referrer to set of weak pointers in this entry.
 * Does not perform duplicate checking (b/c weak pointers are never
 * added to a set twice). 
 *
 * @param entry The entry holding the set of weak pointers. 
 * @param new_referrer The new weak pointer to be added.
 
 函数的两个关键入参, 分别是 oc对象的weak_entry!! 另一个参数是新的指向该oc对象的弱引用指针的地址
 */
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer) {
		// out_of_line() 函数判断 weak_entry中管理 弱引用指针的方式使用的 HashTable 还是定长数组!!!
    if (!entry->out_of_line()) {
				// 表示当前使用的定长数组, 如果定长数组还有空间, 那么直接将弱引用指针的地址存储到定长数组的空位成, 然后返回即可
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }
        // 此时定长数组中没有剩余空间存储 新的 弱引用指针的地址!!!
				// 需要更换底层容器, 将定长数组存储, 改造成 HashTable存储

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

		// 指定一下此时一定是 HashTable 方式存储 weak pointers
    ASSERT(entry->out_of_line());

		// 如果是首次变化成 HashTable存储 或者 已经超过容积因子0.75
		// 需要对HashTable扩容, 增大2 * oldSize. 然后重新 调用 append_referrer 插入 new_referrer
    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }
    // Hash函数!!! 使用的是 weak pointer的地址关联的函数左右散列函数
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    // Hash 碰撞时, 向下找第一个空的位置(注意是一个循环可变长的array)
    while (entry->referrers[index] != nil) {
        hash_displacement++;
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
    }
    // 记录hash碰撞的最大值
    if (hash_displacement > entry->max_hash_displacement) {
        entry->max_hash_displacement = hash_displacement;
    }
    // 存储在找到的index 后面的第一个空位置, 然后存储
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}

/** 
 * Grow the entry's hash table of referrers. Rehashes each
 * of the referrers.
 * 
 * @param entry Weak pointer hash set for a particular object.
 */
__attribute__((noinline, used))
static void grow_refs_and_insert(weak_entry_t *entry, 
                                 objc_object **new_referrer)
{
		// 如果是增长, 必须 weak_entry_t 使用循环可变长array为容器的 HashTable 存储weak pointer的地址
    ASSERT(entry->out_of_line());

    size_t old_size = TABLE_SIZE(entry);
    size_t new_size = old_size ? old_size * 2 : 8;

    size_t num_refs = entry->num_refs;
    weak_referrer_t *old_refs = entry->referrers;
    entry->mask = new_size - 1;
    
    entry->referrers = (weak_referrer_t *)
        calloc(TABLE_SIZE(entry), sizeof(weak_referrer_t));
    entry->num_refs = 0;
    entry->max_hash_displacement = 0;
    
    // 所有的通过 Hash函数重新计算, 插入新的 HashTable中
    for (size_t i = 0; i < old_size && num_refs > 0; i++) {
        if (old_refs[i] != nil) {
            append_referrer(entry, old_refs[i]);
            num_refs--;
        }
    }
    
    // Insert 插入新的 weak ponter 的地址
    append_referrer(entry, new_referrer);
    if (old_refs) free(old_refs);
}

对于这里存储 weak pointers 地址的HashTable的关键信息, 这里再贴一遍:

weak_entry_t总结如下:

  1. 数据结构: weak_entry_t的是一对Pair!!!
  2. Pair的结构: (oc对象: weak pointers), 并且指针都是混淆过的!!!
  3. weak pointers 使用联合体实现,
    1. 如果weak pointers数量不大于4, 那么使用定长数组存储弱引用指针
    2. 如果数量>4, 那么会使用HashTable存储

weak pointers的数量超过4个时, runtime会使用HashTable来存储!!! 它的信息如下:

  1. 数据结构是HashTable散列表
  2. 底层容器: 分配在堆上可变的循环array数组, 每个成员都是weak pointer的混淆结果
  3. 散列函数: 依赖weak pointer的地址!! 也就是new_referrer计算出来的一个index!!!
  4. KV信息: key是上面计算的index, value是指向oc对象weak pointer指针!
  5. 散列冲突: 当发生散列冲突时, 类似于开放寻址法, 查找array的下一个空的位置存储weak pointer
  6. Hash扩展因子 : HashTable初始容量是8, 增加到0.75时, 扩容成原来的2倍

注意: 上面所有的weak pointer 都是混淆结果的代称

5. weak_table_t 弱引用表的删除操作

上面已知了弱引用表的增加操作, 之类代码类似, 删除操作就不详细展开了.

/** 
 * Unregister an already-registered weak reference.
 * //  弱引用之前已经是注册在全局的 弱引用表, 这里需要从表中移除
 * // 从表中移除的操作
 * This is used when referrer's storage is about to go away, but referent
 * isn't dead yet. (Otherwise, zeroing referrer later would be a
 * bad memory access.)
 * Does nothing if referent/referrer is not a currently active weak reference.
 * Does not zero referrer.
 * 
 * FIXME currently requires old referent value to be passed in (lame)
 * FIXME unregistration should be automatic if referrer is collected
 * 
 * @param weak_table The global weak table.
 * @param referent The object.
 * @param referrer The weak reference.
 */
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id) // 新的weak指针的地址
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    // 弱引用实体的指针
    weak_entry_t *entry;

    if (!referent) return;

    // 旧实体对象, 需要传入 referent, 也就是 引用对象
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        // 从实体里面移除弱引用指针
        remove_referrer(entry, referrer);
        
        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_remove(weak_table, entry);
        }
    }

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


/** 
 * Remove old_referrer from set of referrers, if it's present.
 * Does not remove duplicates, because duplicates should not exist. 
 * 
 * @todo this is slow if old_referrer is not present. Is this ever the case? 
 *
 * @param entry The entry holding the referrers.
 * @param old_referrer The referrer to remove. 
 */
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
    if (! entry->out_of_line()) {
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == old_referrer) {
                entry->inline_referrers[i] = nil;
                return;
            }
        }
        _objc_inform("Attempted to unregister unknown __weak variable "
                     "at %p. This is probably incorrect use of "
                     "objc_storeWeak() and objc_loadWeak(). "
                     "Break on objc_weak_error to debug.\n", 
                     old_referrer);
        objc_weak_error();
        return;
    }

    size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (entry->referrers[index] != old_referrer) {
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
        hash_displacement++;
        if (hash_displacement > entry->max_hash_displacement) {
            _objc_inform("Attempted to unregister unknown __weak variable "
                         "at %p. This is probably incorrect use of "
                         "objc_storeWeak() and objc_loadWeak(). "
                         "Break on objc_weak_error to debug.\n", 
                         old_referrer);
            objc_weak_error();
            return;
        }
    }
    entry->referrers[index] = nil;
    entry->num_refs--;
}

6. weak_table_t 弱引用表的清理操作

当弱引用指针指向的oc对象的引用计数为0时, 会被release方法调用dealloc方法, 在dealloc中会调用weak_clear_no_lock方法, 通俗点说, 就是

  1. 需要将弱引用表中的弱引用指针全部指向nil!!!
  2. 将该oc对象的 weak_entry_t 结构体从弱引用表中移除!!!
  3. 可能会对 weak_table的HashTable缩表!!!

/** 
 1. Called by dealloc; 
 2. nils out all weak pointers that point to the provided object so that they can no longer be used.
 * 
 * @param weak_table 
 * @param referent The object being deallocated. 
 */
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);
    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;
    
    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];
        if (referrer) {
            if (*referrer == referent) {
            		// 获取到弱引用指针的地址, 以后,通过地址获取弱引用指针的值并设置成nil!!!
                *referrer = nil;
            } else if (*referrer) {
                _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_t 移除
    // 根据情况判断 弱引用表的HashTable是否要缩表
    weak_entry_remove(weak_table, entry);
}

总结

  1. 全局使用固定长度Array, 通过HashTable 维护 SideTables
  2. 使用oc对象的地址关联的Hash函数, 获取对应的SideTable
  3. 使用SideTable中的spinlock保护oc对象的弱引用表weak_table
  4. 弱引用表weak_table使用HashTable数据结构维护weak_entry_t(oc对象, weak pointers) pair数据
  5. 弱引用表weak_table使用的Hash函数是oc对象的地址关联的Hash函数, 获取对应的weak_entry_t
  6. weak_entry_t根据内部管理的weak pointers的数量, 使用两种方式管理weak pointer
    1. weak pointers 数量少于或等于4时, 使用固定长度数组, 遍历方式管理
    2. weak pointers 数量大于4, 使用HashTable管理, 底部是一个循环可变长array
      1. 它使用weak pointer的地址关联函数计算index
      2. 然后使用开放寻址方式解决Hash冲突

一句话小结!!!

  1. weak指针管理中使用了3个HashTable!!!
  2. 其中两个使用oc对象作为index!!!还有一个使用弱引用指针的地址作为index计算方式
  3. 除了全局SideTables(), 其他两个的HashTable都使用开放寻址法作为Hash碰撞解决方法!!!