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_t
value数组中,每一个Item均为一个地址,即weak对象的地址。
以上就是weak实现原理中所涉及到的所有数据结构,具体关系如下图:
二、weak_table_t
与 weak_entry_t
相关方法
在正式探究weak关键字实现原理之前,先来看一些操作 weak_table_t
与 weak_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_t
从 weak_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_referrers
和 referrers
,那么这两个数组是怎么使用的,答案就在下面的方法中。
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,这三个值代表的含义如下:
- HaveOld:当该值为true时,weak对象此时已有指向的内存对象,需要将该指向清除。
- HaveNew:当该值为true时,weak对象需要指向新的内存对象。
- 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_t
,weak_entry_t
中记录了所有指向该内存对象且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来保证代码事务能力。
七、参考资料
上一篇: Mysterious Bacteria
下一篇: 特教的理性愉悦(ecstasy)