开启生长之旅!这是我参加「日新计划 12 月更文挑战」的第5天,点击检查活动详情

一、storeWeak流程

先看一个比如代码:

@autoreleasepool {
        NYPerson *p = [NYPerson new];
    __weak typeof(p) weakP = p;
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(p)));
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(weakP)));
}

运转检查打印成果:

ios内存管理之weak
new出来的目标RetainCount是1,这个很好了解。但是__weak修饰的weakP为什么RetainCount是2呢?

那么__weak在底层究竟做了什么?

咱们经过断点调试,进入源码看看:

ios内存管理之weak
经过断点打印,得知objc_initWeak(id *location,id newObj)中的location代表weakP指针地址,newObj代表p目标NYPerson.

然后咱们看到了真正的中心办法: storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>(location, (objc_object*)newObj);

ios内存管理之weak

  1. <DontHaveOld, DoHaveNew, DoCrashIfDeallocating> 代表模版参数。
  2. HaveOld 代表weak指针是否指向了一个弱引证
  3. HaveNew 代表weak指针是否需求只向一个新的弱引证
  4. CrashIfDeallocating 代表的是被弱引证的目标是否在析构,假如在析构会error。

检查storeWeak中心代码:

static id
storeWeak(id *location, objc_object *newObj)
{
    //...........................省掉.............................//
  //获取两张表
  SideTable *oldTable;//oldTable
  SideTable *newTable;//newTable
retry:
  if (haveOld) {
    oldObj = *location;//拿到old被弱引证的目标
    oldTable = &SideTables()[oldObj];//在经过这个目标获取oldTable-SideTable表
  } else {
    oldTable = nil;
  }
  if (haveNew) {
    newTable = &SideTables()[newObj];//在经过newObj获取newTable-SideTable表
  } else {
    newTable = nil;
  }
  SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);//设置加锁
  if (haveOld && *location != oldObj) {//haveOld 存在 而且 *location指针指向的老目标 和 oldObj不相同
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable); //解锁
    goto retry;//回到retry 再次判别
  }
  if (haveNew && newObj) {//新的弱引证指向 和 新目标
    Class cls = newObj->getIsa();//拿到isa指向的元类
    if (cls != previouslyInitializedClass && 
      !((objc_class *)cls)->isInitialized()) //判别类没有初始化
    {
      SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);//解锁
      class_initialize(cls, (id)newObj);//初始化 +1
            //...........................省掉.............................//
      goto retry;//从头retry
    }
  }
  if (haveOld) {//假如曾指向老的目标
    weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);//移除在老的weak_table的数据
  }
  if (haveNew) {//假如有一个新引证
    newObj = (objc_object *)
      weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
                 crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
    //增加新的引证和目标到weak_table表中
    // weak_register_no_lock returns nil if weak store should be rejected
    // Set is-weakly-referenced bit in refcount table.
    if (!_objc_isTaggedPointerOrNil(newObj)) {//判别是否是TaggedPointer
      newObj->setWeaklyReferenced_nolock();//设置newObj的weakly_referenced = true;是否被弱引证
    }
    // Do not set *location anywhere else. That would introduce a race.
    *location = (id)newObj;//赋值到weakP指针指向的地址
  }
  SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);//解锁
    //...........................省掉.............................//
  return (id)newObj;
}

小结:

__weak底层履行的是storeWeak函数,它的作用是依据location和newObj参数获取,oldTable和newTable然后判别,假如weak指针之前指向了一个弱引证,就会调用weak_unregister_no_lock将weak指针地址移除。假如weak指针指向了一个新的弱引证,就会调用weak_register_no_lock将weak指针地址增加到目标的弱引证表,经过setWeaklyReferenced_nolock设置newisa.weakly_referenced 为 true;

二、weak原理

上篇文章现已了解了 weak_table_t 的结构及作用了。

这边在进行一些弥补: weak_entry_t *weak_entries; 是一个哈希数组,里面存储弱引证目标的相关信息。

ios内存管理之weak
然后咱们在看weak_register_no_lock函数:

id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
           id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
  //referent -- 被弱引证的目标
  //referrer -- weak指针的地址
  objc_object *referent = (objc_object *)referent_id;
  objc_object **referrer = (objc_object **)referrer_id;
  //判别是否TaggedPointer 是回来referent_id
  if (_objc_isTaggedPointerOrNil(referent)) return referent_id;
  // 确保弱引证目标是可用的。
  if (deallocatingOptions == ReturnNilIfDeallocating ||
    deallocatingOptions == CrashIfDeallocating) {
       //...........................省掉.............................//
  }
  weak_entry_t *entry;
  //被弱引证的referent里面的weak_table中找到weak_entry_t
  if ((entry = weak_entry_for_referent(weak_table, referent))) {
    append_referrer(entry, referrer);// 往weak_entry_t里刺进referrer -- weak指针的地址
  }
  else {
    //没找到weak_entry_t就新建一张
    weak_entry_t new_entry(referent, referrer);
    weak_grow_maybe(weak_table);
    weak_entry_insert(weak_table, &new_entry);//在把new_entry刺进到weak_table中
  }
  return referent_id;
}

weak_register_no_lock增加弱引证函数流程:

  • 假如被弱引证的目标为nil或这是一个TaggedPointer,直接回来,不做任何操作。
  • 假如被弱引证的目标正在析构,则抛出异常。
  • 假如被弱引证的目标不能被weak引证,直接回来nil。
  • 假如目标没有再析构而且可以被weak引证,则调用weak_entry_for_referent办法依据弱引证目标的地址从弱引证表中找到对应的weak_entry_t,假如可以找到则调用append_referrer办法向其间刺进weak指针地址。不然新建一个weak_entry_t。

咱们在看weak_unregister_no_lock函数:

void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
            id *referrer_id)
{
  //referent -- 被弱引证的目标
  //referrer -- weak指针的地址
  objc_object *referent = (objc_object *)referent_id;
  objc_object **referrer = (objc_object **)referrer_id;
  weak_entry_t *entry;
  // 被弱引证的目标 ,不存在回来
  if (!referent) return;
  // 被弱引证的referent里面的weak_table中找到weak_entry_t
  if ((entry = weak_entry_for_referent(weak_table, referent))) {
    remove_referrer(entry, referrer);//从weak_entry_t 中移除weak指针的地址
    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;
        }
      }
    }
    // 4张表中都不存在referrer 指针的地址,而且entry 中现已weak指针已被移除
    if (empty) {
      weak_entry_remove(weak_table, entry);//移除weak_table中的weak_entry_t
    }
  }
}

weak_unregister_no_lock移除弱引证函数流程:

  • referent被弱引证的目标 ,不存在直接回来。
  • 经过weak_entry_for_referent办法在weak_table中找出被弱引证目标对应的weak_entry_t
  • 在weak_entry_t中移除weak指针的地址。
  • 移除元素后,判别此刻weak_entry_t中是否还有元素,假如此刻weak_entry_t现已没有元素了,则需求将weak_entry_t从weak_table中移除。

整理了一个目标sidetable,weak联系图:

ios内存管理之weak

三、weak引证计数问题

从头回到比如1的问题,但是__weak修饰的weakP为什么RetainCount是2呢?

咱们开始断点调试,看看CFGetRetainCount弱引证和强引证有什么区别。

检查强引证汇编:

ios内存管理之weak
检查弱引证汇编:
ios内存管理之weak
看到weakPCFGetRetainCount前会履行objc_loadWeakRetained这个函数。

然后咱们搜索objc_loadWeakRetained这个函数进入源码:

id
objc_loadWeakRetained(id *location)
{
  id obj;
  id result;
  Class cls;
  SideTable *table;
retry:
  // fixme std::atomic this load
  obj = *location;//获取弱引指针指向的目标
  if (_objc_isTaggedPointerOrNil(obj)) return obj;//假如是TaggedPointer直接回来
  table = &SideTables()[obj];//获取目标的SideTable
  table->lock();//加锁
  if (*location != obj) {//指针指指向的目标和obj不相等
    table->unlock();//解锁
    goto retry;
  }
  result = obj;
  cls = obj->ISA();//得到目标的ISA
  if (! cls->hasCustomRR()) {
    // Fast case. We know +initialize is complete because
    // default-RR can never be set before then.
    ASSERT(cls->isInitialized());
    if (! obj->rootTryRetain()) {//rootTryRetain->rootRetain 这里加1了
      result = nil;
    }
  }
  else {
    //...........................省掉.............................//
  }
  table->unlock();
  return result;//局部变量 在arc中会-1
}

持续断点控制台打印:

ios内存管理之weak
咱们在看一个情况:

        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(p)));
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(weakP)));
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(p)));

看打印成果:

ios内存管理之weak
为什么经过objc_loadWeakRetained 函数然后在源码找到了obj->rootTryRetain()所以obj引证计数+1了,源p的引证计数仍是计数1呢?

由于在objc_loadWeakRetained 函数中result是局部变量在arc中,履行完了会在减1.所以影响不到外面的p目标的引证计数。

总结

  1. storeWeak:__weak底层履行的是storeWeak函数,它的作用是依据location和newObj参数获取,oldTable和newTable然后判别(都是sideTable类型)。
    • 假如weak指针之前指向了一个弱引证,就会调用weak_unregister_no_lock将weak指针地址移除。
    • 假如weak指针指向了一个新的弱引证,就会调用weak_register_no_lock将weak指针地址增加到目标的弱引证表
  2. weak原理:就是经过目标的sideTable找到相关的weaktable表在经过weak_unregister_no_lockweak_register_no_lock两个函数增加和移除weak指针地址。
    • weak_unregister_no_lock:假如目标没有再析构而且可以被weak引证,则调用weak_entry_for_referent办法依据弱引证目标的地址从弱引证表中找到对应的weak_entry_t,假如可以找到则调用append_referrer办法向其间刺进weak指针地址。不然新建一个weak_entry_t。
    • weak_register_no_lock:经过weak_entry_for_referent办法在weak_table中找出被弱引证目标对应的weak_entry_t,在weak_entry_t中移除weak指针的地址。
  3. weak引证计数问题:在打印CFGetRetainCount((__bridge CFTypeRef)(weakP))之前履行了objc_loadWeakRetained函数里面有个rootTryRetain()->rootRetain()然后引证计数就+1了。而且不影响外部的p目标的引证计数。