iOS——weak实现原理

Weak基本用法

Weak表示弱引用,用weak修饰,描述的引用对象的计数器并不会增加,并且weak指针在引用的对象被释放时自动置为nil,可以解决循环引用问题。
那么weak的具体实现原理如何呢?

Weak实现原理

iOS是如何实现weak的呢,其实weak的底层是一个hash表,key是所指向对象的指针,valueweak指针的地址数组(因为一个对象可能被多个弱引用指针指向)。

Runtime维护了一张weak表,用来存储某个对象的所有weak指针,

之前探究ARC的时候探究过ARC中weak的底层汇编。
在这里插入图片描述
可以看见weak的一整个周期就是从objc_initWeak开始,等到对象释放后,调用objc_destroyWeak销毁指针。

那么首先就从这个入口 objc_initWeak开始看。

objc_initWeak,objc_destroyWeak

在objc库找一下源码。

id
objc_initWeak(id *location, id newObj)
{
    // newObj 是weak对象地址
    if (!newObj) {
        *location = nil;
        return nil;
    }

    // 调用storeWeak函数 传入模板参数:haveOld = false, haveNew = true, 
    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

可以看见底层是由storeWeak来实现逻辑的,我们了解一下他的模板。

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

其中DontHaveOld、DoHaveNew和DoCrashIfDeallocating都是模板参数,具体含义如下:

enum HaveOld { DontHaveOld = false, DoHaveOld = true }; // 是否有值
enum HaveNew { DontHaveNew = false, DoHaveNew = true }; // 是否有新值
enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
}; // 操作正在释放中的对象是否 Crash
static id 
storeWeak(id *location, objc_object *newObj)
{
    // location是weak指针,newObj是weak将要指向的对象
    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:
    // 根据模板参数的不同执行不同的代码
    // 从SideTables中取出存储弱引用表SideTable(weak_table_t的一层封装)
    if (haveOld) {
        // 获得索引为oldObj存储的值
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        // 更改新值指针,获得以newObj为索引存储的值
        newTable = &SideTables()[newObj];
    } else {
        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.
    // 防止弱引用间死锁
    // 并且通过+initialize初始化构造器保证所有弱引用的isa非空指向
    if (haveNew  &&  newObj) {
        // 获取新对象的isa指针
        Class cls = newObj->getIsa();
        // 如果该对象类还未进行初始化则进行初始化
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            // 对其isa指针进行初始化
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));

            //如果该类已经完成执行+initialize方法是最理想情况
            //如果该类+initialize在线程中
            //例如+initialize正在调用storeWeak方法
            //需要手动对其增加保护策略,并设置previouslyInitializedClass指针进行标记
        
            
            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);
        // 如果弱引用被释放的weak_register_no_lock方法返回nil
             //  设置 isa 标志位 weakly_referenced 为 true
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        // 之前不要设置location对象, 这里需要更改指针指向
        *location = (id)newObj;
    }
    else {
        // 没有新值无需操作
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

具体流程如下所示:
在这里插入图片描述
大概做了以下几件事:
1.从全局的哈希表SideTables中,利用对象本身地址进行位运算后得到对应下标,取得该对象的弱引用表。SideTables是一个 64 个元素长度的散列表,发生碰撞时,可能一个SideTable中存在多个对象共享一个弱引用表。
2.如果有分配新值,则检查新值对应的类是否初始化过,如果没有,则就地初始化。
3.如果 location 有指向其他旧值,则将旧值对应的弱引用表进行注销。
4.如果分配了新值,将新值注册到对应的弱引用表中。将isa.weakly_referenced设置为true,表示该对象是有弱引用变量,释放时要去清空弱引用表。

看到这里可能还是不能理解这一段代码,其中出现了几个新的概念,sideTable
,weak_register_no_lock 和 weak_unregister_no_lock,下面具体解析一下它们的底层原理。

SideTable

为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,虽然名字后面又个"s",但其不过还是一个全局的Hash桶,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。来管理引用计数和weak指针。
之前在探究ARC究竟做了什么的时候,在保存引用计数这里提到过,如果引用计数发生上溢和下溢,会将引用计数存在SideTable类中,今天就来看看这个类内部实现。查看其源码

// MARK: - sideTable类
struct SideTable {
    spinlock_t slock;//保证原子操作的自旋锁
    RefcountMap refcnts;//保存引用计数的散列表
    weak_table_t weak_table;//保存weak引用的全局散列表

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

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

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

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

首先可以看见有三个成员变量.

spinlock_t slock

这个成员变量是一个自旋锁,保证同一时间内只有一个线程访问共享资源,如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。这里主要用来保证操作引用计数不会造成线程竞争而产生错误。

RefcountMap

这个成员变量就是保存引用计数的散列表。看一下下面这张图。
在这里插入图片描述

结合它的定义:typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
可以看出来这个散列表的key是对象的地址,value是引用计数。
当value为0时,RefcountMap会自动清除这条记录。
每个RefcountMap中存储一堆对象的引用计数。

那么查询引用计数的流程是什么呢?

  1. 通过计算对象地址的哈希值, 来从 SideTables 中获取对应的 SideTable. 哈希值重复的对象的引用计数存储在同一个 SideTable 里.
  2. SideTable 使用 find() 方法和重载 [] 运算符的方式, 通过对象地址来确定对象对应的桶. 最终执行到的查找算法是 LookupBucketFor()。这个涉及到了OC底层的存储,之前在学习关联对象的时候,遇见过这个函数,不过关联对象偏向于使用InsertIntoBucket完成工作。 下面分析分析源码。

LookupBucketFor()

value_type& FindAndConstruct(const KeyT &Key) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))
      return *TheBucket;
    return *InsertIntoBucket(Key, ValueT(), TheBucket);
  }
  // 找到了就返回这个对象的桶
  // 没有找到则进行插入操作。
// LookupBucketFor - Lookup the appropriate bucket for Val, returning it in
/// FoundBucket.  If the bucket contains the key and a value, this returns
/// true, otherwise it returns a bucket with an empty marker or tombstone and
/// returns false.
// 为Val查找相应的桶,在/// FoundBucket中返回。如果桶中包含键和值,则返回/// true,否则返回带有空标记或墓碑的桶,并返回false。
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
                     const BucketT *&FoundBucket) const {
  // 获取buckets的首地址
  const BucketT *BucketsPtr = getBuckets();
  // 获取可存储的buckets的总数
  const unsigned NumBuckets = getNumBuckets();

  if (NumBuckets == 0) {
    // 如果NumBuckets = 0 返回 false
    FoundBucket = nullptr;
    return false;
  }

  // FoundTombstone - Keep track of whether we find a tombstone while probing.
  const BucketT *FoundTombstone = nullptr;
  const KeyT EmptyKey = getEmptyKey();
  const KeyT TombstoneKey = getTombstoneKey();
  assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
         !KeyInfoT::isEqual(Val, TombstoneKey) &&
         "Empty/Tombstone value shouldn't be inserted into map!");
  // 计算hash下标
  unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
  unsigned ProbeAmt = 1;
  while (true) {
    // 内存平移:找到hash下标对应的Bucket
    const BucketT *ThisBucket = BucketsPtr + BucketNo;
    // Found Val's bucket?  If so, return it.
    if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
      // 如果查询到`Bucket`的`key`和`Val`相等 返回当前的Bucket说明查询到了
      FoundBucket = ThisBucket;
      return true;
    }

    // If we found an empty bucket, the key doesn't exist in the set.
    // Insert it and return the default value.
    // 如果bucket为空桶,表示可以插入
    if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
      // 如果曾遇到墓碑, 则使用墓碑的位置
      // of the empty bucket we eventually probed to.
      FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
      return false;//找到空占位符, 则表明表中没有已经插入了该对象的桶
    }

    // If this is a tombstone, remember it.  If Val ends up not in the map, we
    // prefer to return it than something that would require more probing.
    // Ditto for zero values.
    //如果找到了墓碑
    if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) &&
        !FoundTombstone)
      FoundTombstone = ThisBucket;  //记录墓碑的位置
    if (ValueInfoT::isPurgeable(ThisBucket->getSecond())  &&  !FoundTombstone)
    //这里涉及到最初定义 typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap, 传入的第三个参数 true
    //这个参数代表是否可以清除 0 值, 也就是说这个参数为 true 并且没有墓碑的时候, 会记录下找到的 value 为 0 的桶
      FoundTombstone = ThisBucket;

    // Otherwise, it's a hash collision or a tombstone, continue quadratic
     //用于计数的 ProbeAmt 如果大于了数组容量, 就会抛出异常
    if (ProbeAmt > NumBuckets) {
      FatalCorruptHashTables(BucketsPtr, NumBuckets);
    }
    // 重新计算hash下标
    BucketNo += ProbeAmt++;
    BucketNo &= (NumBuckets-1);
  }
}


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

weak_table_t weak_table

储存对象弱引用指针的hash表。weak功能实现的核心数据结构。

struct weak_table_t {
    weak_entry_t *weak_entries;  //连续地址空间的头指针, 数组
    //管理所有指向某对象的weak指针,也是一个hash
    size_t    num_entries;  //数组中已占用位置的个数
    uintptr_t mask;  //数组下标最大值(即数组大小 -1)
    uintptr_t max_hash_displacement;  //最大哈希偏移值
};


weak中解除和链接弱引用的实现

首先在之前initWeak里面提到了weak_register_no_lock,这个犯法是用来链接弱引用表的,指向对象注册在弱引用表中。
看看源码:

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)

首先可以看见四个参数:

  • @param weak_table 一个全局的weak表
  • @param referent 弱引用指向的对象
  • @param referrer 弱指针地址
  • 标记最后一个对象是否正在被释放时造成crash
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;// 弱引用变量

    if (!referent  ||  referent->isTaggedPointer()) return referent_id;

    // 确保弱引用对象是否可行
    // ensure that the referenced object is viable
    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);
    }
    // 如果正在释放中,则根据 crashIfDeallocating 判断是否触发 crash
    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_entry_t *entry;
    
    // 如果当前表中有该对象的记录则直接加入该 weak 表中对应记录
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);
    } 
    else {
        
        // 没有在 weak 表中找到对应记录,则新建一个记录
        weak_entry_t new_entry(referent, referrer);
        
        // 查看是否需要扩容
        weak_grow_maybe(weak_table);
        
        // 将记录插入 weak 表中
        weak_entry_insert(weak_table, &new_entry);
    }

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

    return referent_id;
}

上述代码主要功能如下:

  • 判断被指向对象是否可行,也就是判断其是否正在释放,并且会根据crashIfDeallocating判断是否触发crash
  • weak_table中检测是否有被指向对象的entry,如果有的话,直接将该弱引用变量指针加入到该entry
  • 如果没有找到对应的entry,新建一个entry,并将弱引用变量指针地址加入entry,同时检查weaktable是否扩容。

从上可以看出链接弱引用主要利用到了entry,可以联想到,解除也是利用这个。下面看看解除引用的具体实现。

void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    weak_entry_t *entry;
    // 指向对象为空直接返回
    if (!referent) return;

    // 在weak表中查找
    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) {
            // 不为空 将标记记录为false
            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.
}

这段代码的主要流程

  • weak_table中根据找到被引用对象对应的entry,然后将弱引用变量指针referrerentry中移除。
  • 移除弱引用变量指针referrer之后,检查entry是否为空,如果为空将其从weak_table中移除

weak_table

上面提到了一个全局表weak_table,用来保存每一个对象的entry,下面看看具体实现。

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);
    // 获取weak_table中存储所有对象entry的数组
    weak_entry_t *weak_entries = weak_table->weak_entries;
    // 没有直接返回nil
    if (!weak_entries) return nil;
    // hash_pointer 对地址做位运算得出哈希表下标的方式
    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    
    // 遍历weak_table中的weak_entries,比对weak_entries[index].referent对象和referent对象是否相等
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;// 不能超过 weak_table 最大长度限制
        // 回到初始下标,异常报错
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        hash_displacement++;
        // 以及达到最大hash值,说明遍历完毕没找到
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    // 返回weak指针
    return &weak_table->weak_entries[index];
}

上面代码的流程:

  • 通过被引用对象地址计算获得哈希表下标。
  • 检查对应下标存储的是不是我们要找到地址,如果是则返回该地址。
  • 如果不是则继续往下找(线性查找),直至找到。在下移的过程中,下标不能超过weak_table最大长度,同时hash_displacement不能超过记录的max_hash_displacement最大哈希位移。max_hash_displacement是所有插入操作时记录的最大哈希位移,如果超过了,抛出异常。

下面看看如何插入entry

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);
    // 通过哈希算法得到下标
    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_table->num_entries++;
    // 更新最大哈希位移值
    if (hash_displacement > weak_table->max_hash_displacement) {
        weak_table->max_hash_displacement = hash_displacement;
    }
}

看来之前的过程就很容易理解这个插入了,首先根据对象地址计算得到hash值,判断该位置是否为空,不为空往下找,直到找到空位置,将弱引用变量指针存入空位,同时更新weak_table的当前成员数量num_entries和最大哈希位移max_hash_displacement

下面看看移除entry的代码:

static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
    // 释放 entry 中的所有弱引用
    if (entry->out_of_line()) free(entry->referrers);
    // 置空指针
    bzero(entry, sizeof(*entry));
	// 更新 weak_table 对象数量,并检查是否可以缩减表容量
    weak_table->num_entries--;
    weak_compact_maybe(weak_table);
}
  • 释放entry和其中的弱引用变量。
  • 更新 weak_table 对象数量,并检查是否可以缩减表容量

entry 和 referrer

entry以及比较熟悉了,一个对象的弱引用记录,referrer则是代表弱引用变量,每次被弱引用时,都会将弱引用变量指针referrer加入entry中,而当原对象被释放时,会将entry清空并移除。
看看entry的定义:

// MARK: - weak_entry_t
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;// 对象的地址
    union {
        struct {
            weak_referrer_t *referrers;//可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
            //指向 referent 对象的 weak 指针数组
            uintptr_t        out_of_line_ness : 2;// 这里标记是否超过内连边界
            uintptr_t        num_refs : PTR_MINUS_2;// 数组已占大小
            uintptr_t        mask;// 数组大小
            uintptr_t        max_hash_displacement;// 最大hash偏移值
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT]; //只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。
            //当指向这个对象的weak指针不超过4个,则直接使用数组inline_referrers
        };
    };

可以看见苹果这里为了避免数量少使用hash造成性能浪费,使用共用体,做了一个判断,如果weak指针数量小于4就使用inline_referrers[WEAK_INLINE_COUNT]
下面看看entry是如何添加referrer的:

// MARK: - 将weak指针添加到entry的weak指针集合中
/** 
 * 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.
 */
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    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;
            }
        }

        // Couldn't insert inline. Allocate out of line.
        // // 如果 inline_referrers 超出 WEAK_INLINE_COUNT 数量,则执行下面代码
        // 开辟新空间
        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
        // 将原来的引用转移到新空间
        
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[i];
        }
        // 修改 entry 内容及标志位
        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) {
        return grow_refs_and_insert(entry, new_referrer);
    }
    //  // 根据地址计算下标
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    // 该下表位置下不为空,发生 hash 冲突,
    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;
    }
    // // 找到合适下标后存储
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}

entry的结构和weak_table相似,都使用了哈希表,并且使用线性探测法寻找对应位置。在此基础上有一点不同的地方:

  • entry有一个标志位out_of_line,最初时该标志位为falseentry使用的是一个有序数组inline_referrers的存储结构。
  • 当inline_referrers的成员数量超过WEAK_INLINE_COUNTout_of_line标志位变成true,开始使用哈希表存储结构。每当哈希表负载超过 3/4 时会进行扩容。
    下面看看如何去掉weak指针:
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
    if (! entry->out_of_line()) {
        // 未超出 inline_referrers 时直接将对应位置清空
        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;
    }
    // 超出 inline_referrers 的逻辑,也就是weak指针大于4 
    // 计算下标
    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;
        }
    }
    // 找到对应位置后置为nil
    entry->referrers[index] = nil;
    // entry存的weak元素数量减1
    entry->num_refs--;
}

从entry移除referrer的步骤:

  • out_of_line为false时,从有序数组inline_referrers中查找并移除。
  • out_of_line为true时,从哈希表中查找并移除

dealloc

当被引用的对象被释放后,会去检查isa.weakly_referenced标志位,每个被弱引用的对象weakly_referenced标志位都为true。

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
    // 根据指针获取对应 Sidetable
    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();
}

从上面的代码可以看出,在对象释执行dealloc函数时,会检查isa.weakly_referenced标志位,然后判断是否要清理weak_table中的entry

这最后还是走到了前面的remove。

以上就是weak的实现原理。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
weak_ptr是C++11引入的一种智能指针,它可以用来解决shared_ptr的循环引用问题。weak_ptr指向一个shared_ptr管理的对象,但它不会增加该对象的引用计数,也不控制该对象的生命周期。如果shared_ptr被销毁了,那么weak_ptr就会变成空指针。 下面是一个简单的weak_ptr实现: ```c++ template<typename T> class shared_ptr; template<typename T> class weak_ptr { public: weak_ptr() : m_ptr(nullptr), m_refCount(nullptr) {} weak_ptr(const shared_ptr<T>& sp) : m_ptr(sp.m_ptr), m_refCount(sp.m_refCount) { if (m_refCount) { m_refCount->weakCount++; } } weak_ptr(const weak_ptr<T>& wp) : m_ptr(wp.m_ptr), m_refCount(wp.m_refCount) { if (m_refCount) { m_refCount->weakCount++; } } ~weak_ptr() { reset(); } weak_ptr<T>& operator=(const shared_ptr<T>& sp) { reset(); m_ptr = sp.m_ptr; m_refCount = sp.m_refCount; if (m_refCount) { m_refCount->weakCount++; } return *this; } weak_ptr<T>& operator=(const weak_ptr<T>& wp) { reset(); m_ptr = wp.m_ptr; m_refCount = wp.m_refCount; if (m_refCount) { m_refCount->weakCount++; } return *this; } void reset() { if (m_refCount) { m_refCount->weakCount--; if (m_refCount->weakCount == 0 && m_refCount->refCount == 0) { delete m_refCount; delete m_ptr; } } m_ptr = nullptr; m_refCount = nullptr; } shared_ptr<T> lock() const { if (expired()) { return shared_ptr<T>(); } else { return shared_ptr<T>(*this); } } bool expired() const { return use_count() == 0; } long use_count() const { if (m_refCount) { return m_refCount->refCount; } else { return 0; } } private: T* m_ptr; shared_ref_count* m_refCount; }; ``` 在实现中,我们需要维护一个shared_ref_count类,用来计数一个对象的引用次数和weak_ptr的数量。 ```c++ class shared_ref_count { public: shared_ref_count() : refCount(1), weakCount(0) {} long refCount; long weakCount; }; ``` 在weak_ptr的构造函数中,我们需要将weakCount增加。在析构函数中,我们需要调用reset()方法,将weakCount减少,并在refCount和weakCount都为0时释放资源。 在reset()方法中,我们首先将weakCount减少,然后判断refCount和weakCount是否都为0,如果是,则释放资源。 在lock()方法中,我们首先判断是否过期(即use_count()是否为0),如果是,则返回一个空的shared_ptr,否则返回一个新的shared_ptr。 在expired()方法中,我们判断use_count()是否为0。 在use_count()方法中,我们返回refCount的值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值