weak是怎么实现的
在Objective-C中,用__weak
修饰的指针,会在所指向的那个Objective-C对象被释放后,自动指向nil
。
使用__weak
来修饰指针,相比于__unsafe_unretained
,可以帮助程序员减小访问野指针的风险,方便了程序员对内存的管理。
但是,__weak
是如何做到当指向的Objective-C对象被释放后,自动指向nil
的呢?
一个通俗的解释就是,在Objective-C的运行时环境中,维护了一种weak表,这张哈希表用对象的首地址作为键,将由若干个__weak
修饰的指针自身的地址组成的数组作为值。当一个Objective-C对象被释放后,通过这个对象的起始地址来找到所有指向它的__weak
指针,并将它们指向nil
。
本文中关于runtime的源码参考自苹果开源的objc4-680,编译环境为Xcode7.3,clang-703.0.29。
创建一个__weak指针
写一段简单的代码:
NSObject *strongObj = [[NSObject alloc] init];
__weak NSObject *weakObj = strongObj;
在编译后,可以看到objc_initWeak
和objc_destroyWeak
两个函数被调用了。
在开源的runtime源码中找到这两个方法,就可以依次找到weak表的结构。objc_initWeak
和objc_destroyWeak
两个方法,都调用了storeWeak
方法。
这个storeWeak
方法接收两个参数:
template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
其中,location
中存放的是__weak
指针的地址,newObj
中存放的,是__weak
指针即将指向的对象的地址。这个方法会根据传入的参数来更新维护这个weak表。
weak表的结构
提取一部分结构体来帮助理解:
全局有若干(根据平台可能是8或者64)个SideTable
对象,一个对象的地址经过哈希后,可以确定这个对象具体映射到那个SideTable
。
一个SideTable
结构体中包含了一个weak_table
:
struct SideTable {
//.......
weak_table_t weak_table;
//.......
};
这个weak_table
中的weak_entries
指针,指向一个weak_entry_t
类型的数组:
/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
根据对象首地址查找weak_entry的方法为weak_entry_for_referent()
,可以看到其中对对象首地址做了一次哈希,得到了对应的index。如果发生碰撞,则index依次+1,遍历整个数组,检查是否能获得正确的weak_entry_t
对象。
这个weak_entry_t
中维护了所有指向某个地址的__weak
指针:
/**
* The internal structure stored in the weak references table.
* It maintains and stores
* a hash set of weak references pointing to an object.
* If out_of_line==0, the set is instead a small inline array.
*/
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};
其中的weak_referrer_t
实际就是指向Objective-C对象的指针的地址的类型:
/// The address of a __weak object reference
typedef objc_object ** weak_referrer_t;
weak_clear_no_lock
在runtime的代码中,可以找到一个叫做weak_clear_no_lock
的方法,它的注释表明,这个方法会被dealloc方法调用,然后将被销毁对象所对应的所有弱引用指针都置为nil。
/**
* Called by dealloc; nils out all weak pointers that point to the
* provided object so that they can no longer be used.
*
* @param weak_table
* @param referent The object being deallocated.
*/
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);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
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_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
这段代码中做的事情比较清晰,大概流程是:
- 利用即将被销毁的对象的首地址,找到对应的
weak_entry_t
。 - 从
weak_entry_t
对象中获取指向weak_referrer_t
数组的指针。 - 遍历这个数组,对于数组中的每个元素,检查是否真的指向即将被销毁的对象的首地址,如果是,则将它指向nil,如果不是则报错。
- 释放这个
weak_entry_t
对象,并更新weak_table_t
中的entry数目。如果有必要,对weak_table_t
进行缩容。
其他
使用__weak
修饰的指针,还有一些神奇的行为。
当使用一个__weak
指针时,objc_loadWeakRetained()
和objc_release()
函数都被调用了。
其中objc_loadWeakRetained()
会调用retainWeakReference
方法,如果某个类重写了retainWeakReference
方法并返回NO
,则这个__weak
指针获取的就永远是nil
了。
而retainWeakReference
方法默认的实现,是会尝试增加该对象的引用计数。
为什么需要在使用一个__weak
指针时调用objc_loadWeakRetained()
函数呢?注释中是这样说的:
/**
* This function gets called when the value of a weak pointer is being
* used in an expression. Called by objc_loadWeakRetained() which is
* ultimately called by objc_loadWeak(). The objective is to assert that
* there is in fact a weak pointer(s) entry for this particular object being
* stored in the weak-table, and to retain that object so it is not deallocated
* during the weak pointer's usage.
*
* @param weak_table
* @param referrer The weak pointer address.
*/
id
weak_read_no_lock(weak_table_t *weak_table, id *referrer_id)
是为了确保在使用这个__weak
指针的过程中,指向的对象不被释放。