由于iOS采用引用计数技术,后期引入ARC(自动引用计数技术)。针对平台中的循环引用,一般其中对生命周期短的对象采用弱引用计数,可以保证正常对象所在空间释放,并在调用方做特定的判断,保证程序不crash。
一 OC中的_weak
1. 基本数据结构
OC中的惯用_weak修饰符,语句类似如下:
{
id foo = [[NSObject alloc] init];
id __weak boo = foo;}
ObjectC中对对象的存储,实现上做了一定的优化,一旦有弱引用对象被赋值,即运行时(Runtime)会在全局的SideTables中分配一个SideTable空间,此空间是根据对象的地址相关算法获取到的一个位置(所以存在多个对象分配到同一个位置,类似hash表的碰撞)。其中SideTable结构如下:
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
...
}
其中,refcnts是引用计数表,表示分配到此SideTable里所有的对象引用情况, 键值为对象指针,对应value为引用的一些标记位以及状态;weak_table_t是一个散列表,结构如下:
struct weak_table_t {
weak_entry_t *weak_entries; // 保存了所有指向指定对象的 weak 指针
size_t num_entries; // 存储空间,即 entries 的数目
uintptr_t mask; // 参与判断引用计数辅助量
uintptr_t max_hash_displacement; // hash key 最大偏移量
};
根据对象地址相关的hash算法找到对应的SideTable对象,从weak_entries中查找是否已有Entry存储它的信息,如果不存在的话,则增加新的weak_entry_t;如果存在的话,插入对应的weak_entry_t
根据以上数据结构以及NSObject中的源码(函数storeWeak,weak_register_no_lock分析),我们来分析一下下面的示例代码段:
{
id foo = [[NSObject alloc] init];
id __weak bar = foo; [1]
id __weak bar2 = foo; [2] id foo2 = [[NSObject alloc] init]; bar = foo2; [3]}
[1]是将对应的foo的弱引用信息存储到一个新分配的SideTable中,其中会增加一条bar引用;[2]是在weak_entries中增加新的引用到foo分配的SideTable;[3]是替换bar的引用,此时会将foo2增加分配一个新的SideTable,增加一条bar的引用,同时根据bar之前引用对象,找到旧的SideTable,并将bar的引用删除。
其中weak_entry_t定义如下:
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 封装 objc_object 指针,即 weak 修饰的变量指向的对象
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; // hash 元素上限阀值
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};
这个数据结构,在保证指向此应用的弱引用不多于WEAK_INLINE_COUNT时,采用内联静态数组(空间连续)存储,看上去是否有点类似enum数据结构的内部存储技巧;一旦多于WEAK_INLINE_COUNT,此时flag为out_of_line的值是1,分配一个动态数组指向。
2. 生命状态
一旦对象创建出来被变量引用时,比如强引用,调用retain函数,即会从SideTables中找一个SideTable进行信息存储。其中,refcnts中对应的引用计数位置会增加SIDE_TABLE_RC_ONE。此后,如果有对应的弱引用发生,会在对应weak_table中增加信息。
释放的调用链:
sidetable_release ->
sidetable_release_slow ->
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc)
过程中,弱引用减少,会从weak_table中删除对应的引用信息;如果是refcnts对应的对象,如果是deallocating状态,会把该对象从refcnts移除掉。所以SideTables空间作为一个全局内存,会一直存在,没有回收的概念。
c二 Swift中的weak
1. 内存布局
Swift4.0之后,新的weak实现方式也是采用了一种SideTable概念。
对象前4个字节表示class信息,后面4个字节做了特殊处理(标记方式来判断是以下哪一种情况),一般不需要sideTable时,作为引用计数用,一旦需要sideTable,则存放指正指向sideTable。
图片来源参考于文章3
从上图中可以看出,弱引用只指向sideTable,不再指向原来的对象。一旦强引用计数为0时,可以彻底释放原来对象的空间。
Swift对应的sideTable,毕竟所占内存很小,针对每个对象各自管理的,没有一个全局管理器,相对而言也是Swift4出现时,市面上手机内存都很大了吧。
2.对象的生命状态机
分配(new) +弱引用 减少强引用 deinited完成 -弱引用
strongCnt 1 -1 (==0) 0
unownedCnt 1 -1 0
weakCnt 1 +1 -1 -1(==0)
sideTable nil create nil
状态 live live deiniting 是否可以free dead
从以上状态图中可以看出,sideTable是按需产生的,当weak引用计数为0时,sideTable释放回收,
三 关于线程安全性
根据文档与代码,OC中的_weak中SideTable包含一个自旋锁,保证了赋值以及释放操作线程安全。但基于文档中说明,有若干函数有说明“并行修改弱引用变量是线程不安全的,但清除是线性安全的”。(This function IS NOT thread-safe with respect to concurrent modifications to the weak variable. (Concurrent weak clear is safe.))
由于Swift4中,对应的SideTable所在内存空间小,只要是weak RC值不为0时,保持内存(引用的实例已经free掉), 可以保证基本的读与删除或写与删除或读与读操作的线程安全。(The intent is that weak references do not need to be safe against read/write and write/write races. They do need to be safe against read/destroy and write/destroy races (by “destroy”, I mean destruction of the object, not the weak reference). I agree that they should also be safe against read/read races.) 参考文章1,5的线程安全说明。
参考:
1. github.com/apple/swift/stdlib/public/runtime/WeakReference.h
3.maximeremenko.com/swift-arc-w…
5. forums.swift.org/t/thread-sa…