Weak实现原理|青训营笔记

134 阅读8分钟

Weak实现原理|青训营笔记

这是我参与「第四届青训营 」笔记创作活动的第5天

__weak 修饰的对指针在引用对象时,不会增加引用对象的计数,并在引用对象计数归0销毁之后 __weak 修指针会自动将指向对象置为nil。为什么 __weak 可以实现这样的效果

id object = [[NSObject alloc] init];
{
       id __weak objc = obj;
}

初始化时,会调用 objc_initWeak(&objc, object) 方法

objc_initWeak(id *loacation, id newObj)

方法底层源码

/// 初始化__weak修饰符修饰对象
/// @param location: __weak指针的地址,即存储指针的地址,方便在最后的时候将其指向的地址置为nil
/// @param newObj:所引用对象
id objc_initWeak(id *location, id newObj){
//如果引用对象本来就是空,就直接让 __weak指针指向nil
   if (!newObj) {
       *location = nil;
       return nil;
  }
//否则调用storeWeak方法
   return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
      (location, (objc_object*)newObj);
}

storeWeak方法

**template <HaveOld haveOld, HaveNew haveNew, CrashIfDeallocating crashIfDeallocating> static id storeWeak(id location, objc_object newObj)

源代码:有些长,但是配合下面的实现讲解会容易理解一下

核心实现功能

1.将weak指针的地址 location 存入到obj对应的 weak_entry_t 的数组(链表)中,用于在obj析构时,通过该数组(链表)找到所有其weak指针引用,并将指针指向的地址(location)置为nil。

2.如果启用了isa优化,则将obj的 isa_t 的 weakly_referenced 位置为1。置为1的作用主要是为了标记obj被weak引用了,当dealloc时,runtime会根据 weakly_referenced 标志位来判断是否需要查找obj对应的 weak_entry_t ,并将引用置为nil。

实现步骤

1.storeWeak实际上接收了五个参数: haveOldhaveNewcrashIfDeallocatingloacationnewObj

  • haveOld : bool类型,表示weak指针之前是够指向了一个弱引用
  • haveNew:bool类型,表示weak指针是否需要指向一个新指针
  • crashIfDeallocating:bool类型,表示如果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash
  • location:__weak指针的内存地址
  • newObj:弱引用的对象

2.维护了两个SideTable类型的表:

  • oldTable:旧的引用表
  • newTable:新的引用表

3.对oldTable、newTable进行赋值处理

  • 如果 weak指针 之前弱引用过一个 obj ,则将这个 obj 所对应的 SideTable 取出,赋值给oldTable,否则就将 oldTable 置为nil
  • 如果 weak指针 要引用一个新的obj,就把 obj 对应的 sideTable 取出,赋值给 newTable ,否则将 newTable 置为nil。

4.对oldTable、newTable中的weak_entry_t进行维护

  • 如果weak_ptr之前弱引用过别的对象oldObj,则调用 weak_unregister_no_lock(&oldTable->weak_table, oldObj, location),在oldObj的weak_entry_t中移除该weak_ptr地址
  • 如果weak指针需要指向一个新的引用,则会调用weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);方法将weak ptr的地址记录到newObj对应的weak_entry_t中

5.调用setWeaklyReferenced_nolock方法修改weak新引用的对象的bit标志位

// Template parameters.
enum HaveOld { DontHaveOld = false, DoHaveOld = true };
enum HaveNew { DontHaveNew = false, DoHaveNew = true };
enum CrashIfDeallocating {
   DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
​
template <HaveOld haveOld, HaveNew haveNew,
         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) { // 如果weak ptr之前弱引用过一个obj,则将这个obj所对应的SideTable取出,赋值给oldTable
       oldObj = *location;
       oldTable = &SideTables()[oldObj];
  } else {
       oldTable = nil; // 如果weak ptr之前没有弱引用过一个obj,则oldTable = nil
  }
   if (haveNew) { // 如果weak ptr要weak引用一个新的obj,则将该obj对应的SideTable取出,赋值给newTable
       newTable = &SideTables()[newObj];
  } else {
       newTable = nil; // 如果weak ptr不需要引用一个新obj,则newTable = nil
  }
   
   // 加锁操作,防止多线程中竞争冲突
   SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
​
   // location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
   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())  // 如果cls还没有初始化,先初始化,再尝试设置weak
      {
           SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
           _class_initialize(_class_getNonMetaClass(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; // 这里记录一下previouslyInitializedClass, 防止改if分支再次进入
​
           goto retry; // 重新获取一遍newObj,这时的newObj应该已经初始化过了
      }
  }
​
   // Clean up old value, if any.
   if (haveOld) {
       weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); // 如果weak_ptr之前弱引用过别的对象oldObj,则调用weak_unregister_no_lock,在oldObj的weak_entry_t中移除该weak_ptr地址
  }
​
   // Assign new value, if any.
   if (haveNew) { // 如果weak_ptr需要弱引用新的对象newObj
       // (1) 调用weak_register_no_lock方法,将weak ptr的地址记录到newObj对应的weak_entry_t中
       newObj = (objc_object *)
           weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
                                 crashIfDeallocating);
       // weak_register_no_lock returns nil if weak store should be rejected
       
       // (2) 更新newObj的isa的weakly_referenced bit标志位
       // Set is-weakly-referenced bit in refcount table.
       if (newObj  &&  !newObj->isTaggedPointer()) {
           newObj->setWeaklyReferenced_nolock();
      }
​
       // Do not set *location anywhere else. That would introduce a race.
       // (3)*location 赋值,也就是将weak ptr直接指向了newObj。可以看到,这里并没有将newObj的引用计数+1
       *location = (id)newObj; // 将weak ptr指向object
  }
   else {
       // No new value. The storage is not changed.
  }
   
   // 解锁,其他线程可以访问oldTable, newTable了
   SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
​
   return (id)newObj; // 返回newObj,此时的newObj与刚传入时相比,weakly-referenced bit位置1
}

SideTable

SideTable结构入戏

struct SideTable {
   spinlock_t slock;
   RefcountMap refcnts;
   weak_table_t weak_table;
}
  • spinlock_t slock: 互斥锁,用于上锁、解锁sideTable

  • RefcountMap refcnts :用来存储OC对象的引用计数的hash表:

    • 没开启isa优化之前,直接使用refcnts进行计数
    • 开启isa优化之后,尽在 isa 共用体中的 extra_rc 的存储计数溢出时才会向refcnts转移。每次转移一半
  • weak_table_t weakTable:存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构

weak_table_t

weak_table_t 结构如下

struct weak_table_t {
   weak_entry_t *weak_entries;
   size_t    num_entries;
   uintptr_t mask;
   uintptr_t max_hash_displacement;
};
  • weak_entries: 一个动态数组,hash表的容器,用来存储 weak_entry_t类型元素。 weak_entry_t 就是弱引用对象的相关信息
  • num_entries: hash数组中的元素个数
  • mask:hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)
  • max_hash_displacement:可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)

weak_entry_t

weak_entry_t 也是一个哈希表,存储的元素是指向弱引用对象指针的指针,通过操作指针的方式使得 weak指针在引用对象析构之后指向nil

引用该对象的对象列表,联合。 引用个数小于4,用inline_referrers数组。 用个数大于4,用动态数组weak_referrer_t *referrers

struct weak_entry_t {
   DisguisedPtr<objc_object> referent; // 被弱引用的对象
   
   // 引用该对象的对象列表,联合。 引用个数小于4,用inline_referrers数组。 用个数大于4,用动态数组weak_referrer_t *referrers
   union {
       struct {
           weak_referrer_t *referrers;                      // 弱引用该对象的对象指针地址的hash数组
           uintptr_t        out_of_line_ness : 2;           // 是否使用动态hash数组标记位
           uintptr_t        num_refs : PTR_MINUS_2;         // hash数组中的元素个数
           uintptr_t        mask;                           // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
           uintptr_t        max_hash_displacement;          // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
      };
       struct {
           // out_of_line_ness field is low bits of inline_referrers[1]
           weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
      };
  };

stripeMap(SideTables)、sidetable、weak_table_t、wean_entry_t之间的关系

上述的这些都是哈希表结构

  • runtime 中,通过SideTable来管理对象的引用计数以及weak的引用。一张 SideTable 会管理很多个对象,而非一个,而这一个个的 SideTable 又构成一个个集合,叫 SideTables(其类型是 template<typename T> class StripedMap,StripedMap<SideTable> )。一个stipeMap在iOS中有八个 sidetable, 在mac上有 64 个。
id object = [[NSObeject alloc] init];
id __weak obj = object;
  • stripMap:

    • key :__weak 指针指向的地址,即上述代码段的 object 的地址
    • value: 一张sideTable
  • sideTable:

    • Key: __weak 指针指向的地址,即上述代码段的 object 的地址
    • Value: weak_table_t
  • weak_table_t:

    • Key: __weak 指针指向的地址,即上述代码段的 object 的地址
    • Value: weak_entry_t
  • weak_entry_t :

    • Key: __weak 指针的地址
    • value:__weak 指针的地址

总结

所有弱引用obj的指针地址都保存在obj对应的weak_entry_t中。当obj要析构时,会遍历weak_entry_t中保存的弱引用指针地址,并将弱引用指针指向nil,同时,将weak_entry_t移除出weak_table。

1.weak的原理在于维护了一张 weak_table_t 哈希表, key是引用对象的地址,value是weak指针的地址数组

2.weak 关键字的作用是弱引用,所引用对象的计数器不会加1,并在引用对象被释放的时候自动被设置为 nil。、

3.对象释放时,调用clearDeallocating函数根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

参考资料

weak引用底层实现原理

iOS内存机制

iOS底层原理;weak实现原理