ios内存管理之weak

6,119 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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)));
}

运行查看打印结果: image.png new出来的对象RetainCount是1,这个很好理解。但是__weak修饰的weakP为什么RetainCount是2呢?

那么__weak在底层到底做了什么?

我们通过断点调试,进入源码看看: image.png 通过断点打印,得知objc_initWeak(id *location,id newObj)中的location代表weakP指针地址,newObj代表p对象NYPerson.

然后我们看到了真正的核心方法: storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>(location, (objc_object*)newObj); image.png

  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; 是一个哈希数组,里面存储弱引用对象的相关信息。

image.png 然后我们在看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关系图: image.png

三、weak引用计数问题

重新回到例子1的问题,但是__weak修饰的weakP为什么RetainCount是2呢?

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

查看强引用汇编:

image.png 查看弱引用汇编: image.png 看到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
}

继续断点控制台打印: image.png 我们在看一个情况:

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

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

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

看打印结果: image.png 为什么经过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对象的引用计数。