逐步探究ObjC的Weak技术底层
前言
之前的文章有说过 Atomic
原子操作的原理,其作为一个特殊的修饰前缀,影响了存取操作。
在属性修饰定义中,还有另一类修饰前缀,他们分别是 strong
weak
assign
copy
,这些又有什么区别呢?
平时喜欢探究的同学,可能也见过 unsafe_unretained
,这个又是什么呢?
让我们从属性修饰入手,逐步揭开弱引用的面纱。
原理
属性自动生成的实现方法是怎么样的?
首先我们先创建一个示例代码文件作为样本。
#import <Foundation/Foundation.h>
@interface PropertyObject : NSObject
@property (nonatomic, strong) NSObject *pStrongObj; //强引用
@property (nonatomic, copy) NSObject *pCopyObj; //拷贝
@property (nonatomic, weak) NSObject *pWeakObj; //弱引用
@property (nonatomic, assign) NSObject *pAssignObj; //申明
@property (nonatomic, unsafe_unretained) NSObject *pUnretainedObj; //非持有
@end
@implementation PropertyObject
@end
然后通过 clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.14 -fobjc-runtime=macosx-10.14 -Wno-deprecated-declarations main.m
命令将其解释成 c++
代码。(注意这里要指定版本,不然weak属性不能翻译)
展开的代码比较多,我这里截取关键部分探讨。
struct PropertyObject_IMPL {
NSObject *__strong _pStrongObj;
NSObject *__strong _pCopyObj;
NSObject *__weak _pWeakObj;
NSObject *__unsafe_unretained _pAssignObj;
NSObject *__unsafe_unretained _pUnretainedObj;
};
{"pStrongObj","T@\"NSObject\",&,N,V_pStrongObj"},
{"pCopyObj","T@\"NSObject\",C,N,V_pCopyObj"},
{"pWeakObj","T@\"NSObject\",W,N,V_pWeakObj"},
{"pAssignObj","T@\"NSObject\",N,V_pAssignObj"},
{"pUnretainedObj","T@\"NSObject\",N,V_pUnretainedObj"}
从变量结构体的描述和特性可以看出,strong
和copy
实际都是__strong
修饰,但特性不同,assign
和unsafe_unretained
则完全一致,都是__unsafe_unretained
,weak
则单独使用__weak
修饰。
下面我们来看一下方法具体实现。
// @implementation PropertyObject
//根据偏移取值和赋值
static NSObject * _I_PropertyObject_pStrongObj(PropertyObject * self, SEL _cmd) { return (*(NSObject *__strong *)((char *)self + OBJC_IVAR_$_PropertyObject$_pStrongObj)); }
static void _I_PropertyObject_setPStrongObj_(PropertyObject * self, SEL _cmd, NSObject *pStrongObj) { (*(NSObject *__strong *)((char *)self + OBJC_IVAR_$_PropertyObject$_pStrongObj)) = pStrongObj; }
static NSObject * _I_PropertyObject_pCopyObj(PropertyObject * self, SEL _cmd) { return (*(NSObject *__strong *)((char *)self + OBJC_IVAR_$_PropertyObject$_pCopyObj)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
//只有Copy不同,setter的实现是objc_setProperty
static void _I_PropertyObject_setPCopyObj_(PropertyObject * self, SEL _cmd, NSObject *pCopyObj) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct PropertyObject, _pCopyObj), (id)pCopyObj, 0, 1); }
static NSObject * _I_PropertyObject_pWeakObj(PropertyObject * self, SEL _cmd) { return (*(NSObject *__weak *)((char *)self + OBJC_IVAR_$_PropertyObject$_pWeakObj)); }
static void _I_PropertyObject_setPWeakObj_(PropertyObject * self, SEL _cmd, NSObject *pWeakObj) { (*(NSObject *__weak *)((char *)self + OBJC_IVAR_$_PropertyObject$_pWeakObj)) = pWeakObj; }
static NSObject * _I_PropertyObject_pAssignObj(PropertyObject * self, SEL _cmd) { return (*(NSObject *__unsafe_unretained *)((char *)self + OBJC_IVAR_$_PropertyObject$_pAssignObj)); }
static void _I_PropertyObject_setPAssignObj_(PropertyObject * self, SEL _cmd, NSObject *pAssignObj) { (*(NSObject *__unsafe_unretained *)((char *)self + OBJC_IVAR_$_PropertyObject$_pAssignObj)) = pAssignObj; }
static NSObject * _I_PropertyObject_pUnretainedObj(PropertyObject * self, SEL _cmd) { return (*(NSObject *__unsafe_unretained *)((char *)self + OBJC_IVAR_$_PropertyObject$_pUnretainedObj)); }
static void _I_PropertyObject_setPUnretainedObj_(PropertyObject * self, SEL _cmd, NSObject *pUnretainedObj) { (*(NSObject *__unsafe_unretained *)((char *)self + OBJC_IVAR_$_PropertyObject$_pUnretainedObj)) = pUnretainedObj; }
// @end
在代码中,只有copy
修饰属性的setter
方法使用了objc_setProperty
,其他几种都是根据 self + 偏移量
的方式计算出内存地址直接进行存取。
那问题来了,如果真的是那么简单的话,arc
是怎么实现根据不同修饰从而进行内存管理的呢?
原来通过 clang -rewrite-objc
的代码只是翻译成 c++
语言,在之后的编译过程中会进一步处理。
接着使用 clang -S -fobjc-arc -emit-llvm main.m -o main.ll
命令生成中间码。
(中间码显示比较杂乱,我根据自己理解整理成简洁版)
//代码整理后
id [PropertyObject pStrongObj] {
return *location;
}
void [PropertyObject setPStrongObj:](self, _cmd, obj) {
@llvm.objc.storeStrong(*location, obj)
}
id [PropertyObject pCopyObj] {
return @objc_getProperty(self, _cmd, offset, atomic)
}
void [PropertyObject setPCopyObj:](self, _cmd, obj) {
@objc_setProperty_nonatomic_copy(self, _cmd, obj, offset)
}
id [PropertyObject pWeakObj] {
id obj = @llvm.objc.loadWeakRetained(*location)
return @llvm.objc.autoreleaseReturnValue(obj)
}
void [PropertyObject setPWeakObj:](self, _cmd, obj) {
@llvm.objc.storeWeak(*location, obj)
}
id [PropertyObject pAssignObj] {
return *location
}
void [PropertyObject setPAssignObj:](self, _cmd, obj) {
*location = obj
}
id [PropertyObject pUnretainedObj] {
return *location
}
void [PropertyObject setPUnretainedObj:](self, _cmd, obj) {
*location = obj
}
可以看出分别针对strong
和 weak
都做了处理,而assign
和 unsafe_unretained
则不做内存管理直接返回,这也说明这两者的处理方式是一样的,区别在于 assign
针对。
strong | copy | weak | assign | unsafe_unretained | |
---|---|---|---|---|---|
Ownership | __strong | __strong | __weak | __unsafe_unretained | __unsafe_unretained |
Getter | *location | objc_getProperty | loadWeakRetained | *location | *location |
Setter | storeStrong | objc_setProperty | storeWeak | *location | *location |
对象 | NSObject | NSObject | NSObject | NSObject | Scalar |
Weak对象怎么实现存取的?
本文篇幅有限,暂不介绍 storeStrong
和 objc_setProperty_nonatomic_copy
,主要介绍 weak
相关操作。
打开 objc4-750
开源代码,翻到 NSObject.mm
,我们来一探究竟。
// 初始化弱引用
id objc_initWeak(id *location, id newObj) {
// 不存在则不保存
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
// 销毁弱引用
void objc_destroyWeak(id *location) {
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
// 交换原有的值
id objc_storeWeak(id *location, id newObj) {
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}
可以看到 runtime
中调用的都是一个方法,区别在于使用了不同的模版,那么我们来看下对一个地址的存取方法。
// 获取操作的具体实现
id objc_loadWeakRetained(id *location) {
id obj;
id result;
Class cls;
SideTable *table;
retry:
// 保证地址有数据且不是伪指针
obj = *location;
if (!obj) return nil;
if (obj->isTaggedPointer()) return obj;
// 根据地址取出对应的表
table = &SideTables()[obj];
// 加锁
table->lock();
// 如果数据被其他线程改变,则重试
if (*location != obj) {
table->unlock();
goto retry;
}
result = obj;
cls = obj->ISA();
if (! cls->hasCustomRR()) {
// 如果使用的是系统默认的内存管理,则保证了已经初始化
// 所以可以直接rootTryRetain
assert(cls->isInitialized());
if (! obj->rootTryRetain()) {
result = nil;
}
} else {
// 如果不是默认的,则需要确保在初始化线程上执行自定义retain操作
if (cls->isInitialized() || _thisThreadIsInitializingClass(cls)) {
BOOL (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL))
class_getMethodImplementation(cls, SEL_retainWeakReference);
if ((IMP)tryRetain == _objc_msgForward) {
result = nil;
} else if (! (*tryRetain)(obj, SEL_retainWeakReference)) {
result = nil;
}
} else {
table->unlock();
_class_initialize(cls);
goto retry;
}
}
//完成后解锁
table->unlock();
return result;
}
// 保存操作的具体实现
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;
// 由于有锁的机制,如果在期间值被改变了,则重试,直到成功
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;
}
// 检查新对象类有没有初始化完,没有则重试
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized()) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// 如果正在初始化,则让下一次绕过这个判断继续运行
previouslyInitializedClass = cls;
goto retry;
}
}
// 清除之前保存的弱引用数据
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// 保存新的弱引用数据
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// 保存成功就记录到对象指针中,这样可以在释放时检查
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// 保存到对应位置
*location = (id)newObj;
}
// 操作成功后解锁
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// 返回最终数据
return (id)newObj;
}
除去保护方法,其实 objc_loadWeakRetained
方法就是检查后返回 *location
,也就是变量指向的实际地址。
而 storeWeak
方法则是根据模版,对旧对象执行 weak_unregister_no_lock
,对新对象执行 weak_register_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.
// 上面为苹果注释,看这意思应该是objc_storeWeak还需要使用引用地址做后续处理。
}
//注册引用
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; //被引用人
objc_object **referrer = (objc_object **)referrer_id; //引用人
// taggedPointer没有引用计数,不需要处理
if (!referent || referent->isTaggedPointer()) return referent_id;
// 保证被引用人不在释放中,不然闪退
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
} else {
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) {
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))) {
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;
}
//释放过程清空引用
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) {
//这里应该肯定有entry,因为调用前判断了对象的WeaklyReferenced
//如果确实没有,苹果认为可能是CF/objc原因
return;
}
//清空引用数组
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
的变量进行存取操作,其实都是通过被操作的对象地址查找到相应的表,然后增删表的引用数组内容。
SideTable表怎么设计的?
关键就在于怎么申明创建表,以及这个表是怎么设计及使用的。
// SideTables 类型申明
// 这里之所以先使用数据的方式申明是因为考虑到加载顺序的问题
alignas(StripedMap<SideTable>) static uint8_t
SideTableBuf[sizeof(StripedMap<SideTable>)];
// 加载image时执行初始化
static void SideTableInit() {
new (SideTableBuf) StripedMap<SideTable>();
}
// 数组还原成StripedMap类型
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
// StripedMap 的结构
enum { CacheLineSize = 64 };
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
// 64位对齐
struct PaddedT {
T value alignas(CacheLineSize);
};
// 手机系统数组个数为8
PaddedT array[StripeCount];
// 把指针地址匹配到数组的序号
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
}
在加载镜像的过程中,通过 SideTableInit
方法创建全局表数组,可以看到手机系统是8个数组。
源码中使用 &SideTables()[obj]
的方式,其实就是把 obj
的指针地址转成序号获取某一个 table
,通过这种方式分散冗余。
接着我们看 SideTable
类的内部结构。
// 哈希散列表,使用补码的形式把指针地址作为Key,保存引用计数
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
// Template parameters.
enum HaveOld { DontHaveOld = false, DoHaveOld = true };
enum HaveNew { DontHaveNew = false, DoHaveNew = true };
struct SideTable {
spinlock_t slock; // 自旋锁
RefcountMap refcnts; // 引用记数表
weak_table_t weak_table;// 弱引用表
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
struct weak_table_t {
weak_entry_t *weak_entries; //弱引用数组
size_t num_entries; //数组个数
uintptr_t mask; //计算辅助量,数值为数组总数-1
uintptr_t max_hash_displacement;//哈希最大偏移量
};
#if __LP64__
#define PTR_MINUS_2 62
#else
#define PTR_MINUS_2 30
#endif
typedef DisguisedPtr<objc_object *> weak_referrer_t;
struct weak_entry_t {
// 被引用者
DisguisedPtr<objc_object> referent;
union {
// 引用者数据结构
struct {
// 当数量超过4个时,结构转为指针,每次容量满的时候就扩容两倍
// 需要与数组作区分,所以有out_of_line_ness标记
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 {
// 四个数组
weak_referrer_t inline_referrers[4];
};
};
};
SideTable
存储的不仅有对象引用计数表,还有我们关注的弱引用表,其结构顺序如下:
SideTable->weak_table_t->weak_entry_t->weak_referrer_t
为了方便理解,我模拟一下找弱引用对象的步骤:
-
sideTable = &SideTables()[referent]
把对象内存地址按照8取余后找到表 -
weakTable = &sideTable->weak_table
取出弱引用表 -
entry = weak_entry_for_referent(weakTable, referent)
根据被引用人地址,遍历弱引用表找出入口 -
referrer = entry->referrers[index]
入口有特殊的数组,其中保存了所有弱引用者的对象地址
仔细一点的同学应该发现了 weak_entry_t
中有一个联合体,这又是怎么操作实现的呢?
// 添加新引用者
static void append_referrer
(weak_entry_t *entry, objc_object **new_referrer) {
// 没有超过4个,就用内敛数组
if (! entry->out_of_line()) {
// 遍历数组,如果有空位置,则插入后返回
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}
// 如果超过4个了,就从数组结构转成指针结构
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
// 拷贝原数据到指针指向的内容
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; //最大hash移位次数,用于优化循环
// 由于只有4个,会在下个判断后执行grow_refs_and_insert初始化并插入新对象
}
// 断言必然是指针结构
assert(entry->out_of_line());
// 如果指针数量超过3/4,就容量翻倍后再插入
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; //下标+1后取余
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修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?
对象被释放时执行 obj->rootDealloc()
,如果有弱引用标记,则会执行 objc_destructInstance
方法后释放。
void *objc_destructInstance(id obj) {
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj); //调用析构函数
if (assoc) _object_remove_assocations(obj); //移除关联对象关系
obj->clearDeallocating(); //处理isa
}
return obj;
}
inline void objc_object::clearDeallocating() {
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
} else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
void objc_object::sidetable_clearDeallocating() {
SideTable& table = SideTables()[this];
// 删除强引用和弱引用
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
table.unlock();
}
可以看到在 sidetable_clearDeallocating
方法中,最后执行了 weak_clear_no_lock
清空了所有引用关系。
SideTable
表结构如下图:
总结
weak原理是绕不开的经典课题,通过阅读开源代码对苹果如何实现有了大致的了解,受益匪浅。
阅读过程中还惊叹于苹果各种花式小技巧,由于文章篇幅有限没来得及介绍,感兴趣可以了解一下,比如 DisguisedPtr
。