ObjC runtime是如何实现weak指针的
用strong指针创建weak指针,系统会调用objc_initWeak()函数,objc_initWeak中会判断初始化表达式的右值是否为nil,如果为nil就把weak指针指向nil,否则调用storeWeak(),注意不是objc_storeWeak()
如果对weak指针赋值,则通过objc_storeWeak() 函数,通过判断weak指针是否为nil,右值是否为nil,然后调用storeWeak()
storeWeak()
声明:
template
static id storeWeak(id *location, objc_object *newObj);
之所以用template是为了优化性能
操作:
通过HaveOld判断weak指针是否为nil,如果不是,则缓存旧值和通过&SideTables()[xxxObj];取出旧值所在的SideTables
通过HaveNew判断newObj是否为nil,如果不是则缓存newObj的SideTables
对SideTables加锁
如果weak指针指向的值和旧值不一样,则代表已经处理了或者处于多线程data race中,会解锁并从头再来
对newObj做处理,防止弱引用了的对象的isa指向空
如果有旧值,调用weak_unregister_no_lock解除旧值所在weak table对weak指针的持有
解除方式是通过查找旧值的SideTable的weak_table,遍历找到并remove掉weak指针
如果有newObj
调用weak_register_no_lock绑定newObj
如果newObj使用了TaggedPointer优化(比如NSNUmber,会指向常量所以不需要担心内存释放),则直接返回
判断[newObj allowsWeakReference]是否为NO,如果为NO代表newObj在dealloc,此时如果CrashIfDeallocating为true,就会crash
把weak指针绑定到newObj的weak table中,如果newObj没有weak table则创建
如果newObj还活着,weak_register_no_lock就会返回newObj,如果newObj是否isTaggedPointer,会调用newObj的setWeaklyReferenced_nolock()标记一下
把weak指针指向newObj
对SideTables解锁,返回newObj
SideTable是一个C++结构体:
struct SideTable {
spinlock_t slock;
// 保证原子操作的自旋锁
RefcountMap refcnts;
// 引用计数的 hash 表
weak_table_t weak_table;
// weak 引用全局 hash 表
}
struct weak_table_t {
weak_entry_t *weak_entries;
// 保存了所有指向指定对象的 weak 指针,weak_entry_t是节点的结构体
size_t num_entries;
// 存储空间
uintptr_t mask;
// 参与判断引用计数辅助量
uintptr_t max_hash_displacement;
// hash key 最大偏移值
};
使用一个weak指针创建另一个weak指针时,会调用objc_copyWeak()
读取weak 指针时,会通过objc_loadWeakRetained(&weak指针)获取其指向的对象(retain一次),然后把它加到autoreleasepool中
并且!!!访问多少次weak 指针就会调用这么多次objc_loadWeakRetained和添加这么多次autoreleasepool,所以最好在block中拿到weak指针后,用一个__strong指针指向它,避免多余的操作,并且最好直接判断一下是否为nil,是的话返回不做处理
weak指针离开作用域时,会调用objc_destroyWeak() 函数把该指针从weak table中移除
当此对象的引用计数为0的时候会 dealloc, dealloc的最后会调用object_dispose()函数,触发objc_clear_deallocating()函数在 weak 表中获取对象的地址对应的weak指针数组,从而设置为 nil,并且把相关的weak指针从weak table中移除
ps1:
当一个OC对象的dealloc函数被触发调用的时候,是不允许对其进行弱引用的,如果在这时弱引用会引发crash,如:
-(void)dealloc{
__weak __typeof(self) weakSelf = self;
}
ps2:
weak指针的objc_开头的创建/修改函数都不是线程安全的,多线程下极低概率会导致data race crash
swift中的不桥接ObjC的话,weak实现方式会稍微有点不一样:
这里大概讲一下swift3之后的实现:
弱指针基本等同于普通的指针。
弱引用指向对象实例的side table地址
当一个弱引用对象的deinit执行后,对象并没有被释放,且弱引用指针也没有被赋nil。
当弱引用执行完deinit后,访问弱引用对象,则对象指针才会被赋nil,且目标对象被释放。
弱引用对象对于每一个弱引用会包含一个引用计数(unowned计数和strong计数为同一个),且与强引用计数分开统计(但是统一管理的,只有当两者都为0才会被释放)。
这是因为Swift的计数方式默认为InlineRefCounts,当对象只包含strong或unowned引用时,使用InlineRefCounts进行计数管理,当对象拥有了weak引用,InlineRefCounts会变为SideTableRefCounts
也就是说,swift对象的析构和对象的释放不一定是同时的,当Swift对象的strong引用计数变为0但是weak计数大于0时,对象会被析构但是不会被释放内存
底层实现
系统在尝试读取weak指针时,会通过HeapObject *swift::swift_weakTakeStrong(WeakReference *ref)函数操作,该函数最终会来到HeapObject *nativeTakeStrongFromBits(WeakReferenceBits bits) 函数
在这个函数中,会通过getNativeOrNull方法从对象的side table中查询对象的计数,当没有strong引用时,说明该对象已经处于DEINITING状态,函数返回nullptr,接着外层函数会赋空weak引用,weak计数减一,释放剩余的对象内存,并返回nil
否则将调用weak的引用自减计数函数decrementWeak(),再调用refCounts的decrementWeakShouldCleanUp()函数进行位数操作,当weak、strong、unowned计数都变为0时函数会返回true,这时候系统会清空weak指针对应的side table,最后回到nativeTakeStrongFromBits调用tryRetain函数来尝试获取对象
完整实现大致如下:
HeapObject *nativeTakeStrongFromBits(WeakReferenceBits bits) {
auto side = bits.getNativeOrNull();
if (side) {
side->decrementWeak();
return side->tryRetain();
} else {
return nullptr;
}
}
void decrementWeak() {
bool cleanup = refCounts.decrementWeakShouldCleanUp();
if (!cleanup)
return;
assert(refCounts.getUnownedCount() == 0);
delete this;
}
同时,添加weak对象会使对象的引用计数管理会从InlineRefCounts替换为SideTableRefCounts,这也会带来一定的开销,对于有性能要求的场景swift提供了unowned,unowned的行为跟strong是一样的,但不会使计数增加,代价是对象被释放了的话,访问unowned指针就是未定义的行为(相当于ObjC中的访问野指针)