欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS weak关键字实现原理

程序员文章站 2022-06-02 12:39:06
...

在iOS中,使用weak关键字能够对内存对象进行弱引用,基于这个特性,使用weak关键字能够解决许多问题,例如delegate中对象的循环持有问题、Block对对象的强引用导致的对象无法及时释放问题。

为何weak关键字能够实现对内存对象的弱引用,今天我们就来探究一下。

首先在分析weak关键字实现原理之前,先介绍一下相关的数据结构,这些数据结构其中一部分可能在其他地方有所提及,但本文只列出与weak关键字有关的一部分。

这些数据结构全部存在于runtime源码中,相关内容可以在 objc-weak文件中查看。

一、数据结构

1. SideTables

SideTables本质上是一个全局的 StripedMap

StripedMap本质是一个数组,且在iOS系统下,容量为64。

该数据结构通过实现[]操作,实现了类似字典的功能:可通过传入一个对象作为key值,来获取对应的Item。

SideTables中, Item类型为 SideTable,由此可见,对于任何一个对象, SideTables都能根据其地址对应到具体的一个 SideTable上。

2. SideTable

SideTable中包含三个元素,分别是 1.自旋锁 2.记录对象引用计数的字典 3.记录对象弱引用信息的数据结构 weak_table_t

其中 weak_table_t是与weak关键字有关的数据结构,其余二者暂可不用关注。

3. weak_table_t

weak_table_t本质上是一个数组,其中每个Item为 weak_entry_t

4. weak_entry_t

weak_entry_t就比较有意思了,它本质上是个字典。

其中的key值为对象,而value对应为一个数组,该数组最初为内部的一个大小为4的数组,当数组大小超过4后,则变为内部一个可变大小数组。

无论value值对应的数组是固定大小还是可变大小,数组中保存的值均为 weak_referrer_t类型的数据。

5. weak_referrer_t

weak_referrer_t本质上是 objc_object **,即Objective-C对象的地址。

所以,weak_entry_tvalue数组中,每一个Item均为一个地址,即weak对象的地址。

以上就是weak实现原理中所涉及到的所有数据结构,具体关系如下图:

iOS weak关键字实现原理

二、weak_table_tweak_entry_t相关方法

在正式探究weak关键字实现原理之前,先来看一些操作 weak_table_tweak_entry_t的方法。

1. 从 weak_table_t中查询对应的 weak_entry_t

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    //获取weak_table_t的数组结构
    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

    //获取对象地址,并根据地址映射到数组结构长度内,得到对应下标
    size_t index = hash_pointer(referent) & weak_table->mask;
    //线性探寻数组结构中对应的value所在index
    size_t hash_displacement = 0;
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    //返回查询到的weak_entry_t
    return &weak_table->weak_entries[index];
}

2. 向 weak_table_t中增加新的 weak_entry_t

static void 
weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
    //获取weak_table_t的数组结构
    weak_entry_t *weak_entries = weak_table->weak_entries;
    assert(weak_entries != nil);

    //获取对象地址,并根据地址映射到数组结构长度内,得到对应下标
    size_t index = hash_pointer(new_entry->referent) & (weak_table->mask);
    //线性探寻数组结构中value所应在的位置
    size_t hash_displacement = 0;
    while (weak_entries[index].referent != nil) {
        index = (index+1) & weak_table->mask;
        hash_displacement++;
    }

    //将```weak_entry_t```放入```weak_table_t```对应位置,并更新相关数据
    weak_entries[index] = *new_entry;
    weak_table->num_entries++;

    if (hash_displacement > weak_table->max_hash_displacement) {
        weak_table->max_hash_displacement = hash_displacement;
    }
}

3. 扩展 weak_table_t容积

weak_entry_insert方法不需要考虑 weak_table_t容积,因为runtime代码中在调用 weak_entry_insert方法前都会调用 weak_grow_maybe方法来在必要的时候扩展 weak_table_t容积。

static void 
weak_grow_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    //当weak_table_t容积超过3/4时,进行容积扩展
    if (weak_table->num_entries >= old_size * 3 / 4) {
        //weak_table_t容积扩展为原先容积的2倍,且保证了最小容积为64
        weak_resize(weak_table, old_size ? old_size*2 : 64);
    }
}

4. 从 weak_table_t中移除 weak_entry_t

static void 
weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
    //移除weak_entry_t中相关数据
    if (entry->out_of_line) free(entry->referrers);
    
    //将weak_entry_t所在位置内存全部重置,相当于将weak_table_t数组结构对应位置置为NULL,等同于从数组结构中移除
    bzero(entry, sizeof(*entry));
    
    //weak_table_t中数据个数减一
    weak_table->num_entries--;

    //调用weak_compact_maybe方法进行必要的压缩
    weak_compact_maybe(weak_table);
}

5. 压缩 weak_table_t

weak_entry_tweak_table_t中移除后,runtime会对 weak_table_t进行必要的压缩,减少内存的使用。

static void 
weak_compact_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    //当weak_table_t容积大于1025,并且其中有效数据个数少于容积的1/16时,进行压缩
    if (old_size >= 1024  && old_size / 16 >= weak_table->num_entries) {
        //将weak_table_t容积压缩到之前的1/8,保证压缩后有效个数尽量占有新容积的1/2但不超过1/2
        weak_resize(weak_table, old_size / 8);
        // leaves new table no more than 1/2 full
    }
}

6. 向 weak_entry_t中添加新的 objc_object **

之前我们介绍 weak_entry_t时提到它本身是一个字典,key为内存对象,value为所有指向该内存对象的weak对象数组,这个数组有两个,分别为 inline_referrersreferrers,那么这两个数组是怎么使用的,答案就在下面的方法中。

static void 
append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    //若out_of_line标志位为false,即表明应向inline_referrers数组中插入数据
    if (! entry->out_of_line) {
        //尝试向inline_referrers数组中插入weak对象,若有空位并插入成功,则直接退出
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }
        
        //若向inline_referrers数组中插入失败,则开始启用inferrers数组
        //首先将inline_referrers数组中数据用于初始化inferrers数组
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[i];
        }
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        //置out_of_line标志位为true,表明数组数据存在于inferrers数组中
        entry->out_of_line = 1;
        //初始化线性探寻所需要的数据
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    assert(entry->out_of_line);

    //若inferrers数组有效数据超过容积的3/4时,调用grow_refs_and_insert方法扩展inferrers数组
    //grow_refs_and_insert方法扩展容积的关键代码为old_size ? old_size * 2 : 8,即容积扩展为原先的2倍,且保证最小为8
    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }
    
    //使用线性探寻,找到应当存放weak对象的位置,并将weak对象插入
    size_t index = w_hash_pointer(new_referrer) & (entry->mask);
    size_t hash_displacement = 0;
    while (entry->referrers[index] != NULL) {
        index = (index+1) & entry->mask;
        hash_displacement++;
    }
    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++;
}

7. 从 weak_entry_t中移除 objc_object **

static void 
remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
    //若out_of_line标志位为false,即表明数组数据保存在inline_referrers数组中
    if (! entry->out_of_line) {
        //从inline_referrers数组中找到对应weak对象并删除
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == old_referrer) {
                entry->inline_referrers[i] = nil;
                return;
            }
        }
        return;
    }

    //线性探寻,找到weak对象所在位置,并从referrers数组中删除
    size_t index = w_hash_pointer(old_referrer) & (entry->mask);
    size_t hash_displacement = 0;
    while (entry->referrers[index] != old_referrer) {
        index = (index+1) & entry->mask;
        hash_displacement++;
        if (hash_displacement > entry->max_hash_displacement) {
            return;
        }
    }
    entry->referrers[index] = nil;
    entry->num_refs--;
}

从代码中可见,weak_entry_t在移除对象后,并不会进行类似 weak_table_t压缩数据结构的操作,故应尽量保证weak对象个数较少。

三、weak关键字修饰对象初始化及重新赋值实现原理

介绍完weak关键字涉及到的数据结构,接下来就该分析weak关键字实现原理了,我们先从weak关键字修饰对象初始化开始分析。

1. objc_initWeak

NSObject.mm文件中,有一个 objc_initWeak方法,官方文档描述为:当初始化一个weak对象并将内存对象赋值给该weak对象时会调用该方法。

id 
objc_initWeak(id *location, id newObj)
{
    //对内存对象进行非nil判断
    if (!newObj) {
        *location = nil;
        return nil;
    }
    
    //调用storeWeak方法,三个参数的意义在storeWeak方法中描述
    return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
        (location, (objc_object*)newObj);
}

2. objc_storeWeak

NSObject.mm文件中,有一个 objc_storeWeak方法,官方文档描述为:当为一个weak对象赋新值时会调用该方法。

id 
objc_storeWeak(id *location, id newObj)
{
    //调用storeWeak方法,三个参数的意义在storeWeak方法中描述
    return storeWeak<true/*old*/, true/*new*/, true/*crash*/>
        (location, (objc_object *)newObj);
}

3. storeWeak

由官方文档可知,无论是初始化weak对象还是为weak对象赋值,最终都会调用到 storeWeak方法,不同点在于,二者传入三个模板值不同。

storeWeak有三个模板值:HaveOld,HaveNew,CrashIfDeallocating,这三个值代表的含义如下:

  1. HaveOld:当该值为true时,weak对象此时已有指向的内存对象,需要将该指向清除。
  2. HaveNew:当该值为true时,weak对象需要指向新的内存对象。
  3. CrashIfDeallocating:当该值为true时,若内存对象正在被释放或不支持弱引用(-(BOOL)allowsWeakReference或-(BOOL)retainWeakReference方法返回NO),立刻crash;当该值为false时,返回nil。
id 
storeWeak(id *location, objc_object *newObj)
{
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

 retry:
    //获取weak对象所指的旧内存对象以及旧内存对象所对应的SideTable
    if (HaveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    //获取新内存对象所对应的SideTable
    if (HaveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    //调用相关方法清除weak对象与旧内存对象的关联
    if (HaveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    //调用相关方法将weak对象放入新内存对象所对应的```weak_entry_t```value数组中
    if (HaveNew) {
        newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, 
                                                      (id)newObj, location, 
                                                      CrashIfDeallocating);
        
        //将新内存对象weakly_referenced标志位置为true
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        //将weak对象指向新内存对象
        *location = (id)newObj;
    }
    
    return (id)newObj;
}

4. weak_unregister_no_lock

我们先来看一下weak对象是如何与旧内存对象解除指向关系的。

void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id)
{
    //内存对象
    objc_object *referent = (objc_object *)referent_id;
    //weak对象
    objc_object **referrer = (objc_object **)referrer_id;

    weak_entry_t *entry;

    if (!referent) return;
    
    //获取内存对象对应的weak_entry_t
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //调用remove_referrer方法,将weak对象从内存对象对应的weak_entry_t中删除
        remove_referrer(entry, referrer);
        
        
        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;
                }
            }
        }
        
        //若weak_entry_t在删除之后有效数据为空,则表明该内存对象没有任何weak对象指向,可以将对应的weak_entry_t从weak_table_t中删除,并在必要条件下对weak_table_t进行空间压缩
        if (empty) {
            weak_entry_remove(weak_table, entry);
        }
    }
}

5. weak_register_no_lock

在将weak对象与旧内存对象的关联解除后,就调用 weak_register_no_lock方法将weak对象与新内存对象进行关联。

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;
    //weak对象
    objc_object **referrer = (objc_object **)referrer_id;

    //tagged类型对象无需进行处理
    if (!referent  ||  referent->isTaggedPointer()) return referent_id;
    
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        //若内存对象所属类没有自定义retain/release/dealloc/allowsWeakReference等方法,可以直接读取当前是否正在释放
        deallocating = referent->rootIsDeallocating();
    }
    else {
        //若内存对象所属类自定义了retain/release/dealloc/allowsWeakReference等方法,读取该类是否支持弱引用,若不支持弱引用,直接标识对象为正在释放
        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);
    }

    //若判断当前对象正在释放
    if (deallocating) {
        //crashIfDeallocating为true,则直接crash;crashIfDeallocating为false,返回nil
        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;
        }
    }

    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //若该内存对象对应的weak_entry_t已经在weak_table_t中存在,则直接调用append_referrer将weak对象插入weak_entry_t中
        append_referrer(entry, referrer);
    } 
    else {
        //若该内存对象对应的weak_entry_t不存在,则创建weak_entry_t,并初始化weak_entry_t的inline_referrers,将weak对象放入weak_entry_t的inline_referrers数组第一位
        weak_entry_t new_entry;
        new_entry.referent = referent;
        new_entry.out_of_line = 0;
        new_entry.inline_referrers[0] = referrer;
        for (size_t i = 1; i < WEAK_INLINE_COUNT; i++) {
            new_entry.inline_referrers[i] = nil;
        }
        
        //将weak_entry_t插入weak_table_t中,在此之前对weak_table_t做必要的扩容
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }

    return referent_id;
}

至此,weak关键字修饰对象的初始化和重新赋值流程就完成了,本质来说,每个内存对象都会在全局的 SideTables中对应至一个 SideTable中, SideTable中的 weak_table_t记录了该 SideTable下所有内存对象weak引用信息,内存对象可在 weak_table_t中找到与自己一一对应的 weak_entry_tweak_entry_t中记录了所有指向该内存对象且weak修饰的对象信息。

weak关键字修饰对象的初始化和重新赋值流程如下图:

iOS weak关键字实现原理

四、使用weak关键字修饰对象时原理

weak关键字修饰的对象,在使用时可以访问到所指的内存对象,但是如果是直接使用该内存对象,当在多线程情况下,并不能保证内存对象在weak对象执行语句中被释放,那么weak关键字是如何保证在weak对象执行语句时内存对象不被释放的呢?其实很简单,就是对内存对象进行计数增加。

每次在使用weak对象时,都相当于调用一次 objc_loadWeak

id objc_loadWeak(id *location)
{
    if (!*location) return nil;
    //使用时,计数+1,并在合适aoturelease_pool中进行-1
    return objc_autorelease(objc_loadWeakRetained(location));
}

id objc_loadWeakRetained(id *location)
{
    id result;

    SideTable *table;
    
    table = &SideTables()[result];
    
    //根据weak对象找到所指向的内存对象
    result = weak_read_no_lock(&table->weak_table, location);

    return result;
}

id weak_read_no_lock(weak_table_t *weak_table, id *referrer_id) 
{
    //weak对象
    objc_object **referrer = (objc_object **)referrer_id;
    //内存对象
    objc_object *referent = *referrer;
    //tagged对象无需处理
    if (referent->isTaggedPointer()) return (id)referent;

    //weak对象指向为nil或内存对象没有对应的weak_entry_t,即没有weak对象指向时,返回nil
    weak_entry_t *entry;
    if (referent == nil  ||  
        !(entry = weak_entry_for_referent(weak_table, referent))) 
    {
        return nil;
    }

    
    if (! referent->ISA()->hasCustomRR()) {
        //若内存对象所属类没有自定义retain/release/dealloc/allowsWeakReference等方法,则调用rootTryRetain直接对对象计数+1
        if (! referent->rootTryRetain()) {
            return nil;
        }
    }
    else {
        //若内存对象所属类自定义了retain/release/dealloc/allowsWeakReference等方法,会调用tryRetain方法,并返回该方法返回值
        BOOL (*tryRetain)(objc_object *, SEL) = (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_retainWeakReference);
        if ((IMP)tryRetain == _objc_msgForward) {
            return nil;
        }
        if (! (*tryRetain)(referent, SEL_retainWeakReference)) {
            return nil;
        }
    }

    return (id)referent;
}

由此可见,在weak对象执行的语句中,weak对象所指向的内存对象计数会+1,这样就保证在语句中不会发生执行一半而释放内存对象的问题。

五、weak关键字修饰对象所指内存对象释放时

除了文章开头提到的特征外,weak关键字还具有一个特征:当weak对象指向的内存对象被释放后,weak对象自动置为nil。

那底层原理是如何实现的呢?

当内存对象释放时,会一次调用以下方法:

_objc_rootDealloc
rootDealloc
object_dispose
objc_destructInstance
clearDeallocating
sidetable_clearDeallocating
weak_clear_no_lock

weak_claer_no_lock方法中,会进行对weak对象的置空操作:

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    //内存对象
    objc_object *referent = (objc_object *)referent_id;
    
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    //若内存对象对应的weak_entry_t不存在,则无需做更多操作
    if (entry == nil) {
        return;
    }
    
    //对weak_entry_t存储weak对象的数组中有效数据依次置nil
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}

至此,我们知道了内存对象在释放时所做的操作,也知道了weak对象是在内存对象dealloc时被置为nil的。

但是,如果我们在MRC下,强制重写内存对象的dealloc方法,使之无法正常调用[super dealloc],意味着内存对象无法正常调用到 weak_clear_no_lock,也就无法完成weak对象的置nil,而此时再去获取weak对象,发现获取到的值已经为nil了,这是为什么呢?

在使用weak对象时,当调用到 weak_read_no_lock方法时,我们知道,若内存对象有自定义retain/release/dealloc/allowsWeakReference等方法时,会直接返回tryRetain方法的返回值。

objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    
        if (tryRetain && newisa.deallocating) goto tryfail;

 tryfail:
    if (!tryRetain && sideTableLocked) sidetable_unlock();
    return nil;
}

由此可见,当内存对象被标记为deallocting,即使在还没调用dealloc等方法时,对该对象进行计数+1,也会被返回nil,这就解释了上面的问题。

六、Weak-Strong搭配使用的误解

在使用Block时,我们可以使用weak关键字来避免外部变量被Block强引用而导致的循环引用,同时为了Block中的代码能够正常执行,许多开发者提出了Weak-Strong搭配使用的方式,类似如下:

{
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf test1];
        [strongSelf test2];
    };
}

以上代码相对于单独使用weak来说还是有好处的,在单独使用weak时,可以保证在执行 [weakSelf test1][weakSelf test2]单条语句时,weakSelf所指的self不会被释放或self已经释放而直接向nil发送消息。

若使用Weak-Strong搭配的方式的话,可以保证在执行 [strongSelf test1][strongSelf test2]时,是向同一对象发送消息。

为什么这么说呢?当开始执行Block语句时,若self还存在,那么strongSelf可以保证在整个Block代码块中不会被释放,即使Block中调用无数次strongSelf,strongSelf也不会因为多线程而在半途被释放;若开始执行Block时,self已经被释放,那么之后所有的消息都会被发送至nil。所以Weak-Strong搭配可以保证Block中语句被处理为一个事务。

所以说,Weak-Strong并不能保证Block中语句一定会被执行,它只能保证Block中语句作为一个事务被发送到同一对象处。只要理解了weak实现原理,我们就能明白何时单独使用weak也能完成代码功能,而何时必须使用Weak-Strong来保证代码事务能力。

七、参考资料

  1. iOS 从源码深入探究weak的实现
相关标签: iOS开发