深入分析弱引用(__weak)
__weak 在开发中经常用到,主要是为了解决内存管理中的循环引用。__weak修饰的指针特性是其指向的对象销毁后,会自动置为 nil。
用到__weak基本上分两种情况,一种是先声明再赋值(@property (weak)也是这种),另一种是声明的时候赋值。
NSObject *obj = [NSObject new];
__weak id weak;
weak = obj;
__weak id weak1 = obj;
新建个项目,在项目中写入以上代码,然后再终端输入以下命令编译成C++源码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 "替换为写代码的文件"
可得到以下内容,发现__weak 对应的类型属性都为__attribute((objc_ownership(weak))),代表以weak弱引⽤的⽅式管理对象所有权。
__attribute__((objc_ownership(weak))) id weak;
weak = obj;
__attribute__((objc_ownership(weak))) id week2 = obj;
然后再在源码中打断点,运行进入调试模式。再通过Xcode菜单Debug->Debug Workflow->Always Show Disassembly 进入到汇编模式。
查看汇编。如下
可以看出,先声明后赋值调用的是objc_storeWeak。声明的时候赋值,调用的是objc_initWeak。为什么同样是weak指针,实现逻辑却不一样呢。
在objc源码中找到这两个方法的源码。如下
id objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
唯一不同的地方是,当对象不存在时,把指针设置为nil,可以防止坏内存访问。可以看出location存储着weak指针的地址。而第一种没有这个操作的原因是编译器第一次使用这个指针的时候,已经做了这个操作。
然后看后面调用了两次objc_destroyWeak,这是因为离开了局部变量的范围了,每一个弱指针都需要有一次调用。代码如下
void objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
可以看到这三个方法都调用了storeWeak方法,查看源码,函数的实现太长,这里分段分析。
// Update a weak variable.
// If HaveOld is true, the variable has an existing value
// that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be
// assigned into the variable. This value might be nil.
// If CrashIfDeallocating is true, the process is halted if newObj is
// deallocating or newObj's class does not support weak references.
// If CrashIfDeallocating is false, nil is stored instead.
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template <HaveOld haveOld, HaveNew haveNew,
enum CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
函数前面的注释,表面了模板中三个参数的作用,这里就不做翻译了。函数开始的这部分,主要是变量声明,这里主要注意下SideTable类型。查代码可以看到结构体定义如下(省略了结构体函数)。
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
其中 slock 是一个自旋锁,对 SideTable 实例进行操作时用来加锁。refcnts是当对象的引用计数大于isa指针所能存储的数值时,存放引用计数的地方。weak_table 则是存放弱引用指针的地方(后面将详细分析 weak_table_t)。
继续看storeWeak函数
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
这一段是获取oldOb,以及从SideTables里面分别通过oldObj和newObj的地址作为key获取到了oldTable和newTable,然后上锁。
SideTables返回的是StripedMap<SideTable>类型的哈希表,表的长度在iPhone上是8,模拟器和macOS是64,如下
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
//省略下面内容
}
继续storeWeak函数
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(cls, (id)newObj);
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
看注释可以得出,这里是确保对象的类已经完成+initialize流程。
// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (!_objc_isTaggedPointerOrNil(newObj)) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
如果这个弱指针本身指向旧的对象,先通过weak_unregister_no_lock方法,其从 oldTable 的 weak_table 中移除。如果有新的值,则使用 weak_register_no_lock 方法将其注册到 newTable 的 weak_table 中,并使用 setWeaklyReferenced_nolock 函数将对象标记为被弱引用过。
到这里,对storeWeak大概流程已经分析完成 ,其重点就在 weak_register_no_lock 和 weak_unregister_no_lock 函数上。这两个函数都用到了weak_table,先看下weak_table_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; //weak_entry_t的数组
size_t num_entries; //数组中weak_entry_t的数量
uintptr_t mask; //哈希后的值取余算索引
uintptr_t max_hash_displacement; //哈希碰撞后最大的位移值
};
再看下weak_entry_t结构如下(省略结构体方法)
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
}
第一个DisguisedPtr<objc_object>其实就是 objc_object*,DisguisedPtr是为了是为了躲过内存泄漏工具的检查。
// DisguisedPtr<T> acts like pointer type T*, except the
// stored value is disguised to hide it from tools like `leaks`.
第二个是两个结构体的联合体,先分析第一种。
referrers是一个指向weak_referrer_t 的数组指针,也就是objc_object *的数组指针
typedef DisguisedPtr<objc_object *> weak_referrer_t;
out_of_line_ness占用2 bit 标记位,用来确定联合里的内存是第一个结构体还是第二个结构体;
num_refs:PTR_MINUS_2 是字节长度减去 2 位,和 out_of_line_ness 一起组成一个字节,用来存储 referrers 的大小;
mask 和 max_hash_displacement做哈希表用到的东西。
然后看第二种,inline_referrers是一个长度为WEAK_INLINE_COUNT(4)的数组。
通过搜索代码可以找到append_referrer方法里面区分两种结果的存在时机
if (! entry->out_of_line()) {
// Try to insert inline.
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}
// Couldn't insert inline. Allocate out of line.
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
// This constructed table is invalid, but grow_refs_and_insert
// will fix it and rehash it.
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;
entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
entry->mask = WEAK_INLINE_COUNT-1;
entry->max_hash_displacement = 0;
}
当weak_referrer_t的数量大于4个的时候,用哈希表存储,反之数组存储。
现在回到weak_register_no_lock方法,去掉内存管理相关内容代码如下
id weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
if (_objc_isTaggedPointerOrNil(referent)) return referent_id;
// now remember it and where it is being stored
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
首先获取referent和referrers,referent 是被弱引用的对象,referrers是存储弱引用变量的地址。再判断referent是否为TaggedPointer或者nil,是的话直接返回。
然后用 weak_entry_for_referent 函数搜索对象是否已经有了 weak_entry_t 类型的条目,有的话则使用 append_referrer 把referrer添加到里面,没有的话新建一个 weak_entry_t 条目,然后使用 weak_grow_maybe 函数判断是否需要扩大弱引用表的大小,再使用 weak_entry_insert 将弱引用插入表中。
先看append_referrer函数
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
if (! entry->out_of_line()) {
// Try to insert inline.
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}
// Couldn't insert inline. Allocate out of line.
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
// This constructed table is invalid, but grow_refs_and_insert
// will fix it and rehash it.
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;
entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
entry->mask = WEAK_INLINE_COUNT-1;
entry->max_hash_displacement = 0;
}
ASSERT(entry->out_of_line());
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
return grow_refs_and_insert(entry, new_referrer);
}
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != nil) {
hash_displacement++;
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
}
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++;
}
大概逻辑就是,先用weak_entry_t中的数组存储,如果满了就换哈希表存储。
再看weak_grow_maybe函数
#define TABLE_SIZE(entry) (entry->mask ? entry->mask + 1 : 0)
static void weak_grow_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
// Grow if at least 3/4 full.
if (weak_table->num_entries >= old_size * 3 / 4) {
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}
可以看到,当 weak_table 里的弱引用数量到它容量的四分之三时,便会将容量拓展为两倍。值得注意的是第一次拓展也就是当 mask 为 0 的情况,初始值是 64。
最后是weak_entry_insert函数
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
ASSERT(weak_entries != nil);
size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
hash_displacement++;
}
weak_entries[index] = *new_entry;
weak_table->num_entries++;
if (hash_displacement > weak_table->max_hash_displacement) {
weak_table->max_hash_displacement = hash_displacement;
}
}
先判断哈希表是否存在,不存在证明弱引用的数量不大于4个,还使用数组存储。存在的话就是哈希表插入的过程。
下面看weak_unregister_no_lock
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
weak_entry_t *entry;
if (!referent) return;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
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;
}
}
}
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
// Do not set *referrer = nil. objc_storeWeak() requires that the
// value not change.
}
先使用 weak_entry_for_referent 函数找到对应的弱引用条目,并用 remove_referrer 将对应的弱引用变量位置从中移除。最后判断条目是否为空,为空则使用 weak_entry_remove 将其从弱引用表中移除。
remove_referrer函数实现如下
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
if (! entry->out_of_line()) {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == old_referrer) {
entry->inline_referrers[i] = nil;
return;
}
}
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != old_referrer) {
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
hash_displacement++;
if (hash_displacement > entry->max_hash_displacement) {
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
}
entry->referrers[index] = nil;
entry->num_refs--;
}
先判断是否是inline存储(! entry->out_of_line()),如果是的话把old_referrer直接设置为nil (entry->inline_referrers[i] = nil),不是的话遍历哈希表referrers找到索引,再通过索引设置为nil(entry->referrers[index] = nil),然后将长度-1。
然后看weak_entry_remove方法
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
// remove entry
if (entry->out_of_line()) free(entry->referrers);
bzero(entry, sizeof(*entry));
weak_table->num_entries--;
weak_compact_maybe(weak_table);
}
直接的清零 entry,并给 weak_table 的 num_entries 减 1,最后调用weak_compact_maybe方法检查看weak_table是否需要缩小。
static void weak_compact_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
// Shrink if larger than 1024 buckets and at most 1/16 full.
if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries) {
weak_resize(weak_table, old_size / 8);
// leaves new table no more than 1/2 full
}
}
缩小的话则是需要表本身大于等于 1024 并且存放了不足十六分之一的条目时,直接通过weak_resize方法缩小 8 倍。
static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
size_t old_size = TABLE_SIZE(weak_table);
weak_entry_t *old_entries = weak_table->weak_entries;
weak_entry_t *new_entries = (weak_entry_t *)
calloc(new_size, sizeof(weak_entry_t));
weak_table->mask = new_size - 1;
weak_table->weak_entries = new_entries;
weak_table->max_hash_displacement = 0;
weak_table->num_entries = 0; // restored by weak_entry_insert below
if (old_entries) {
weak_entry_t *entry;
weak_entry_t *end = old_entries + old_size;
for (entry = old_entries; entry < end; entry++) {
if (entry->referent) {
weak_entry_insert(weak_table, entry);
}
}
free(old_entries);
}
}
weak_resize 函数的过程就是新建一个数组,将老数组里的值使用 weak_entry_insert 函数添加进去,mask是新数组长度-1,max_hash_displacement 和 num_entries 也都清零了,因为 weak_entry_insert 函数会对这两个值进行操作。
到这里,弱引用的整个流程已经分析完成了。现在看下为什么对象销毁后,弱引用变量被置为 nil。通过搜索找到对象在是否的时候调用了以下方法
void
objc_object::clearDeallocating_slow()
{
ASSERT(isa().nonpointer && (isa().weakly_referenced || isa().has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa().weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa().has_sidetable_rc) {
table.refcnts.erase(this);
}
table.unlock();
}
如果对象有弱引用会调用weak_clear_no_lock方法
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,然后从中获取到存储的位置和数量,再遍历设置为nil,最后从weak_table中删除。
通过分析流程可以得知弱引用的存储结构 SideTables->SideTable->weak_table_t->weak_entry_t->weak_referrer_t数组或者哈希表。