iOS weak详解
weak
关键字的作用是弱引用,所引用对象的计数器不会加一,引用对象被释放时自动置nil
。
一、weak实现结构
SideTables
共有64个节点,每个节点是一个散列表(SideTable
),结构如下图
SideTable
SideTable
里面包含了一个自旋锁、一个引用计数表、一张weak
引用表
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
……
……
};
-
pinlock_t slock
:uint32_t
类型的非公平的自旋锁。非公平就是说获得锁的顺序和申请锁的顺序无关,第一个申请锁的线程可能最后才获得到该锁,或者是刚获得锁的线程会再次立刻获得到该锁,造成饥饿等待。在OC
中,_os_unfair_lock_opaque
也记录了获取它的线程信息,只有获得该锁的线程才能够解开这把锁。 -
RefcountMap refcnts
:用来存储对象的引用计数。它实质上是一个以objc_object
为key
的hash
表,其vaule
就是对象的引用计数。同时,当对象的引用计数变为0时,会自动将相关的信息从hash
表中剔除。 -
weak_table_t weak_table
: 用来存储OC对象弱引用的相关信息,详见下文
weak_table_t
weak_table
是典型hash
结构。weak_entry_t *weak_entries
是一个动态数组,存储的是weak_entry_t
弱引用信息- 每个
weak_entry_t
弱引用信息对应一个OC对象
struct weak_table_t {
// 弱引用信息动态数组
weak_entry_t *weak_entries;
// 数组重的元素个数
size_t num_entries;
// hash数组长度-1,会参与hash计算。
uintptr_t mask;
// 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数不会超过此值)
uintptr_t max_hash_displacement;
};
weak_entry_t
每个weak_entry_t
实例对应了一个OC对象
,内部有一个数组 objc_object **new_referrer
存储 weak_referrer_t
(指向弱引用该对象的指针),通过操作其指向可以使得weak
引用的指针在对象析构后,指向nil
。
详见注解
#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2
#if __LP64__
#define PTR_MINUS_2 62
#else
#define PTR_MINUS_2 30
#endif
struct weak_entry_t {
// 弱引用对象指针地址取反
DisguisedPtr<objc_object> referent;
// 引用该对象的对象列表,联合。
// 弱引用该对象的指针数目小于等于 WEAK_INLINE_COUNT,使用定长数组 weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]。
// 弱引用该对象的指针数目超过 WEAK_INLINE_COUNT,将定长数组中的元素转移到动态数组 weak_referrer_t *referrers且之后都是用动态数组存储
// 为什么做定长/动态数组的切换?考虑到弱引用的指针个数一般不会超过`WEAK_INLINE_COUNT`个。这时候使用定长数组,不需要动态的申请内存空间,而是一次分配一块连续的内存空间。这会得到运行效率上的提升。
union {
struct {
// 弱引用该对象的对象指针地址的hash数组
weak_referrer_t *referrers;
// 是否使用动态hash数组标记位
uintptr_t out_of_line_ness : 2;
// hash数组中的元素个数
uintptr_t num_refs : PTR_MINUS_2;
// hash数组长度-1,会参与hash计算。
// ps:这里是hash数组的长度,而不是元素个数。数组长度可能是64而元素仅存了2个。
uintptr_t mask;
// 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数不会超过此值)
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];
};
};
// 用来判断当前的是使用的定长数组还是动态数组
// 根据REFERRERS_OUT_OF_LINE注解:inline_referrers[1] 中存储的是 pointer-aligned DisguisedPtr,其最低位一定是 0b00 或 0b11 ,因此可以用 0b10 表示使用了动态数组
// 返回true,此时使用的动态数组
// 返回false,使用静态数组
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
// 赋值方法
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
// 构造方法,里面初始化了静态数组
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
二、weak的初始化
如下代码,创建一个NSObject
对象obj1
,生成另一个weak
标识的NSObject
对象obj2
指向obj1
objc_initWeak
断点进入汇编可以看到调用了objc_initWeak
方法
objc_initWeak
里面调用的是storeWeak
storeWeak
storeWeak
会先判断类是否初始化,如果没有则将其初始化然后retry
。接下来判断是否存在旧值需要删除,最后添加上新值。
storeWeak
太长就不贴图了,附上storeWeak
的代码和注解
// HaveOld: true - 变量有值
// false - 需要被及时清理,当前值可能为 nil
// HaveNew: true - 需要被分配的新值,当前值可能为 nil
// false - 不需要分配新值
// CrashIfDeallocating: true - 说明 newObj 已经释放或者 newObj 不支持弱引用,该过程需要暂停
// false - 用 nil 替代存储
template <HaveOld haveOld, HaveNew haveNew,
enum CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
// 该过程用来更新弱引用指针的指向
ASSERT(haveOld || haveNew);
if (!haveNew) ASSERT(newObj == nil);
// 初始化 previouslyInitializedClass 指针
Class previouslyInitializedClass = nil;
id oldObj;
// 声明两个 SideTable 新旧散列创建
SideTable *oldTable;
SideTable *newTable;
// 获得新值和旧值的锁存位置(用地址作为唯一标示)
// 通过地址来建立索引标志,防止桶重复
// 下面指向的操作会改变旧值
retry:
if (haveOld) { // 更改指针,获得以 oldObj 为索引所存储的值地址
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) { // 更改新值指针,获得以 newObj 为索引所存储的值地址
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 加锁操作,防止多线程中竞争冲突
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
// 避免线程冲突重处理
// location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// 防止弱引用间死锁
// 并且通过 +initialize 初始化构造器保证所有弱引用的isa不是空
if (haveNew && newObj) {
// 获得新对象的 isa 指针
Class cls = newObj->getIsa();
// 判断 isa 非空且已经初始化
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
// 解锁
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// 对其 isa 指针进行初始化
class_initialize(cls, (id)newObj);
// 如果该类已经完成执行 +initialize 方法是最理想情况
// 如果该类 +initialize 在线程中
// 例如 +initialize 正在调用 storeWeak 方法
// 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记
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 ? CrashIfDeallocating : ReturnNilIfDeallocating);
// 如果弱引用被释放 weak_register_no_lock 方法返回 nil
// 在引用计数表中设置若引用标记位
if (!newObj->isTaggedPointerOrNil()) {
// 弱引用位初始化操作
// 引用计数那张散列表的weak引用对象的引用计数中标识为weak引用
newObj->setWeaklyReferenced_nolock();
}
// 之前不要设置 location 对象,这里需要更改指针指向
*location = (id)newObj;
}
else {
// 没有新值,则无需更改
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
//必须在没有锁的情况下调用,因为它可以调用任意代码
//ps:即使_setWeaklyReferenced没有实现,resolveInstanceMethod:也可能被调用并且回调到弱引用机制中
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
weak_register_no_lock
storeWeak
需要的是分配新值的操作weak_register_no_lock
。
该方法先记录下若引用的对象及其指针,确保弱引用对象有值并且不处于析构过程后,尝试获取weak_table
中存储的旧实例weak_entry_t
。
- 如果存在旧实例就通过
append_referrer
方法将引用对象添加到旧实例 - 如果不存在就新建
weak_entry_t
并通过weak_entry_insert
插入weak_table
中
weak_register_no_lock
源码解析
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
// referent_id是新的被弱引用对象
objc_object *referent = (objc_object *)referent_id;
// referrer_id是__weak指针的地址
objc_object **referrer = (objc_object **)referrer_id;
// 如果referent为nil 或 referent 采用了TaggedPointer计数方式,直接返回,不做任何操作
if (referent->isTaggedPointerOrNil()) return referent_id;
// 确保被引用的对象可用(没有在析构,同时应该支持weak引用)
if (deallocatingOptions == ReturnNilIfDeallocating ||
deallocatingOptions == CrashIfDeallocating) {
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
// Use lookUpImpOrForward so we can avoid the assert in
// class_getInstanceMethod, since we intentionally make this
// callout with the lock held.
auto allowsWeakReference = (BOOL(*)(objc_object *, SEL))
lookUpImpOrForwardTryCache((id)referent, @selector(allowsWeakReference),
referent->getIsa());
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, @selector(allowsWeakReference));
}
// 正在析构的对象,不能够被弱引用
if (deallocating) {
if (deallocatingOptions == 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;
}
}
}
// now remember it and where it is being stored
weak_entry_t *entry;
// 在 weak_table中找到referent对应的weak_entry,并将referrer加入到weak_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;
}
weak_entry_for_referent
那么如何从weak_table
中定位到所需的 weak_entry_t
?
在weak_register_no_lock
里其实有调用到这个函数,就是weak_entry_for_referent
。
下面附上代码和注释
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
ASSERT(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
// 确保index不会越界,这和取余操作是一样的,用位操作替代可以提升效率。
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
// 开始对比hash表中的数据是否与目标数据相等
while (weak_table->weak_entries[index].referent != referent) {
// 如果不相等,则index +1,
index = (index+1) & weak_table->mask;
// index == begin 绕了一圈,抛出错误
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
// 超过了hash冲突最大值 return nil
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
接下来看看 weak_entry
是如何插入引用对象的
三、添加弱引用对象
在弱引用实例weak_entry
被创建以后,会判断弱引用表是否存在该实例。
- 如果存在就会走
append_referrer
- 不存在则创建后走
weak_entry_insert
两个函数处理相似,下面以append_referrer
为例做解析
append_referrer
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
if (! entry->out_of_line()) {
// 如果weak_entry 尚未使用动态数组
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}
// 如果inline_referrers的位置已经存满了,则要转型为referrers,做动态数组。
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());
// 如果动态数组中元素个数大于或等于数组位置总空间的3/4,则扩展数组空间为当前长度的一倍
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
// 扩容并插入
return grow_refs_and_insert(entry, new_referrer);
}
// 开始插入到weak_entry中
// 确保begin的位置只能大于或等于 数组的长度
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
// 初始的hash index
size_t index = begin;
// 用于记录hash冲突的次数,也就是hash再位移的次数
size_t hash_displacement = 0;
// 这里 weak_entry_t 的hash算法和 weak_table_t 的hash算法是一样的,
while (entry->referrers[index] != nil) {
hash_displacement++;
// 移到下一个位置,再试一次能否插入。
// 这里entry->mask取值,一定是:0x111, 0x1111, 0x11111, ...
// 因为数组每次都是*2增长,即8, 16, 32,对应动态数组空间长度-1的mask,也就是前面的取值
index = (index+1) & entry->mask;
// 意味着走完都没有合适位置,抛出异常
if (index == begin) bad_weak_table(entry);
}
if (hash_displacement > entry->max_hash_displacement) {
// 记录最大的hash冲突次数
// max_hash_displacement是允许冲突的次数,如果超过都没位置就是出问题了
entry->max_hash_displacement = hash_displacement;
}
// 将ref存入hash数组
weak_referrer_t &ref = entry->referrers[index];
// 更新元素个数num_refs
ref = new_referrer;
entry->num_refs++;
}
至此,弱引用对象已经成功添加。
四、移除弱引用对象
接下来看看当对象被释放时,弱引用机制是怎么运行的。
rootDealloc
rootDealloc
逻辑如下:
首先判断 object
是否采用了 Tagged Pointer
计数,如果是则不进行任何析构操作。
接下来判断是否满足下列条件
- 对象采用了优化的
isa
计数方式(isa.nonpointer
) - 对象没有被
weak
引用(!isa.weakly_referenced
) - 没有关联对象(
!isa.has_assoc
) - 没有自定义的
C++
析构方法(!isa.has_cxx_dtor
) - 没有用到
sideTable
来做引用计数 (!isa.has_sidetable_rc
) 如果都满足则能用函数free(this)
快速释放内存。
其余的,则进入object_dispose((id)this)
慢释放分支。
object_dispose
调用 objc_destructInstance
objc_destructInstance
调用 clearDeallocating
clearDeallocating
调用clearDeallocating_slow
weak_clear_no_lock
clearDeallocating_slow
调用weak_clear_no_lock
清理weak_table
,同时将所有weak
引用置为nil
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);
}
至此,移除流程结束~
五、总结
最后,画了几张简图作为总结
底层结构
添加流程
移除流程
转载自:https://juejin.cn/post/7046329429928181790