请稍侯

iOS weak 底层实现原理

04 December 2020

iOS weak 底层实现原理

weak的原理在于底层维护了一张weak_table_t结构的hash表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。

weak 的实现原理可以概括一下三步:

  1. 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址;
  2. 添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
  3. 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

相关类及函数调用关系图

SideTable、weak_table_t、weak_entry_t 之间的关系

如下图: SideTable、weak_table_t、weak_entry_t

弱引用对象初始化流程

weak原理详解

当一个对象做weak引用,在运行时会在底层库调用objc_initWeak函数; 当对象的引用计数为0时,底层会调用_objc_rootDealloc方法对对象进行释放,而在_objc_rootDealloc方法里面会调用rootDealloc方法

objc_initWeak

该方法的两个参数location和newObj。 location :__weak指针的地址,存储指针的地址,这样便可以在最后将其指向的对象置为nil。 newObj :所引用的对象。即例子中的obj 。 在该方法内部调用了storeWeak方法。

storeWeak

这个方法的实现如下:

  • storeWeak方法实际上是接收了5个参数,分别是haveOld、haveNew和crashIfDeallocating,这三个参数都是以模板的方式传入的,是三个bool类型的参数。 分别表示weak指针之前是否指向了一个弱引用,weak指针是否需要指向一个新的引用,若果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash。
  • 该方法维护了oldTable和newTable分别表示旧的引用弱表和新的弱引用表,它们都是SideTable的hash表。
  • 如果weak指针之前指向了一个弱引用,则会调用weak_unregister_no_lock方法将旧的weak指针地址移除。
  • 如果weak指针需要指向一个新的引用,则会调用weak_register_no_lock方法将新的weak指针地址添加到弱引用表中。
  • 调用setWeaklyReferenced_nolock方法修改weak新引用的对象的bit标志位。

这个方法中的重点也就是weak_unregister_no_lock和weak_register_no_lock这两个方法。而这两个方法都是操作的SideTable这样一个结构的变量。

  • SideTable SideTable 的定义:
     struct SideTable {
        spinlock_t slock;
        RefcountMap refcnts;
        weak_table_t weak_table;
    }
    
  • spinlock_t slock : 自旋锁,用于上锁/解锁 SideTable。
  • RefcountMap refcnts :用来存储OC对象的引用计数的 hash表(仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。
  • weak_table_t weak_table : 存储对象弱引用指针的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;
    • num_entries: hash数组中的元素个数;
    • mask:hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个);
    • max_hash_displacement:可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值); weak_table_t是一个典型的hash结构。weak_entries是一个动态数组,用来存储weak_entry_t类型的元素,这些元素实际上就是OC对象的弱引用信息。

    • weak_entries
      • weak_entry_t weak_entry_t的结构也是一个hash结构,其存储的元素是弱引用对象指针的指针, 通过操作指针的指针,就可以使得weak 引用的指针在对象析构后,指向nil。
          #define WEAK_INLINE_COUNT 4
          #define REFERRERS_OUT_OF_LINE 2
      		  
          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];
                  };
              };
      		  
              bool out_of_line() {
                  return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
              }
      		  
              weak_entry_t& operator=(const weak_entry_t& other) {
                  memcpy(this, &other, sizeof(other));
                  return *this;
              }
      		  
              weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
                  : referent(newReferent) // 构造方法,里面初始化了静态数组
              {
                  inline_referrers[0] = newReferrer;
                  for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
                      inline_referrers[i] = nil;
                  }
              }
          };
      
    • 在weak_entry_t的结构定义中有联合体,在联合体的内部有定长数组inline_referrers[WEAK_INLINE_COUNT]和动态数组weak_referrer_t *referrers两种方式来存储弱引用对象的指针地址。
    • 通过out_of_line()这样一个函数方法来判断采用哪种存储方式。
    • 当弱引用该对象的指针数目小于等于WEAK_INLINE_COUNT时,使用定长数组。
    • 当超过WEAK_INLINE_COUNT时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储。

    weak_entry_for_referent 取元素; append_referrer 添加元素;

  • weak_register_no_lock weak_register_no_lock 方法添加弱引用。这个方法需要传入四个参数,它们代表的意义如下:
    • weak_table:weak_table_t结构类型的全局的弱引用表。
    • referent_id:weak指针。
    • *referrer_id:weak指针地址。
    • crashIfDeallocating :若果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash。

    该方法主要的做了如下几个方便的工作:

    1. 如果referent为nil 或 referent 采用了TaggedPointer计数方式,直接返回,不做任何操作。
    2. 如果对象正在析构,则抛出异常。
    3. 如果对象不能被weak引用,直接返回nil。
    4. 如果对象没有再析构且可以被weak引用,则调用weak_entry_for_referent方法根据弱引用对象的地址从弱引用表中找到对应的weak_entry,如果能够找到则调用append_referrer方法向其中插入weak指针地址。否则新建一个weak_entry。
  • weak_unregister_no_lock weak_unregister_no_lock移除引用。如果weak指针之前指向了一个弱引用,则会调用weak_unregister_no_lock方法将旧的weak指针地址移除。

_objc_rootDealloc

rootDealloc

rootDealloc实现:

inline 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);
    }
}
  1. 首先判断对象是否是Tagged Pointer,如果是则直接返回。
  2. 如果对象是采用了优化的isa计数方式,且同时满足对象没有被weak引用!isa.weakly_referenced、没有关联对象!isa.has_assoc、没有自定义的C++析构方法!isa.has_cxx_dtor、没有用到SideTable来引用计数!isa.has_sidetable_rc则直接快速释放。
  3. 如果不能满足2中的条件,则会调用object_dispose方法。
  • object_dispose object_dispose方法很简单,主要是内部调用了objc_destructInstance方法。

    • objc_destructInstance
        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_assocations(obj);
              obj->clearDeallocating();
          }
      	  
          return obj;
      }
      

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

      • clearDeallocating
        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中的引用计数数据。
      • 如果对象采用了优化isa引用计数,则判断是都有使用SideTable的辅助引用计数(isa.has_sidetable_rc)或者有weak引用(isa.weakly_referenced),符合这两种情况中一种的,调用clearDeallocating_slow方法。

        • clearDeallocating_slow 在这个方法里面调用了weak_clear_no_lock来做weak_table的清理工作。

          • weak_clear_no_lock

此处更详细参考:iOS底层原理:weak的实现原理