iOS weak关键字实现原理

在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实现原理中所涉及到的所有数据结构,具体关系如下图:

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关键字修饰对象的初始化和重新赋值流程如下图:

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的实现
发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/103881352