iOS——weak修饰符的学习补充
Weak修饰符的内部机制
SideTable
ObjectC中对对象的存储,实现上做了一定的优化,一旦有弱引用对象被赋值,即运行时(Runtime)会在全局的SideTables中分配一个SideTable空间,此空间是根据对象的地址相关算法获取到的一个位置(所以存在多个对象分配到同一个位置,类似哈希冲突)。其中SideTable结构如下:
struct SideTable {
//SideTable的结构
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
//
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void reset() { slock.reset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
slock:自旋锁,用于多线程环境下的同步。
RefcountMap:引用计数映射表,管理对象的引用计数。键值为对象指针,对应value为引用的一些标记位以及状态
weak_table:弱引用表,管理对象的弱引用。是一个散列表。
我们先来看一下weak_table_t
的结构:
struct weak_table_t {
weak_entry_t *weak_entries;//hash数组,用来存储弱引用对象的相关信息weak_entry_t。
size_t num_entries;//hash数组中的元素个数。
uintptr_t mask;//参与判断引用计数辅助量。hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)。
uintptr_t max_hash_displacement;//可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)。
};
这个SideTable中,比较重要的就是RefcountMap
和weak_table
,这俩都是用来记录引用计数的散列表。其中RefcountMap
的作用是记录SideTable
中对象的强引用的引用计数,而weak_table
,是用于存储弱引用的,并且在必要的时候更新对象的弱引用,比如说对象被释放的时候更新该对象的所有弱引用为nil,或者当该对象的弱引用指向其他对象的时候也要更新;
我们来看一下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];
};
};
};
它使用了一种优化空间使用的方式。
当弱引用的数量不多时,使用结构体内部的一个固定大小的数组(inline_referrers)。这类似于枚举中的小数据范围直接内联存储在结构体中,避免了动态内存分配的开销。
当弱引用的数量超过固定大小时,转而使用指向动态分配内存的指针(referrers),这类似于在需要更大存储空间时,枚举或其他结构体使用指针指向外部分配的存储区域。
其中out_of_line的值通常情况下是等于零的,所以弱引用表总是一个objc_objective指针数组,当超过4时, 会变成hash表。
生命状态
一个对象被创建出来后,如果被强引用就增加SideTable
中的refcnts
的信息,如果被弱引用就增加weak_table
的信息。如果弱引用减少,会从weak_table
中删除对应的引用信息;如果是refcnts
对应的对象,如果是deallocating
状态,会把该对象从refcnts
移除掉。所以SideTables
空间作为一个全局内存,会一直存在,没有回收的概念。
Weak的工作流程
weak的实现原理概括为以下三步:
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,清理对象的记录。
Weak的初始化
runtime会调用objc_initWeak
函数,objc_initWeak
函数会初始化一个新的weak指针指向对象的地址。
- objc_initWeak:
// location指针objc , newObj原始对象object
id objc_initWeak(id *location, id newObj) {
// 查看原始对象实例是否有效
// 无效对象直接导致指针释放
if (!newObj) {
*location = nil;
return nil;
}
// 这里传递了三个 bool 数值
// 使用 template 进行常量参数传递是为了优化性能
return storeWeak<false/*old*/, true /*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}
添加引用:
objc_initWeak函数会调用objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
// HaveOld: true - location指向了一个旧对象
// false - location当前没有指向任何对象,或者旧对象已经被清理
// HaveNew: true - 需要被分配的新值,当前值可能为 nil
// false - 不需要分配新值
// CrashIfDeallocating: true - 说明 newObj 已经释放或者 newObj 不支持弱引用,该过程需要暂停
// false - 用 nil 替代存储
template bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj) {
// 该过程用来更新弱引用指针的指向
// 初始化 previouslyInitializedClass 指针
Class previouslyInitializedClass = nil;
id oldObj;
// 声明两个 SideTable
// ① 新旧散列创建
SideTable *oldTable;
SideTable *newTable;
// 获得新值和旧值的锁存位置(用地址作为唯一标示)
// 通过地址来建立索引标志,防止桶重复
// 下面指向的操作会改变旧值
retry:
// 如果weak ptr之前弱引用过一个obj,则将这个obj所对应的SideTable取出,赋值给oldTable,即获取其旧的Table
if (HaveOld) {
// 更改指针,获得以 oldObj 为索引所存储的值地址
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else { // 如果weak ptr之前没有弱引用过一个obj,则oldTable = nil
oldTable = nil;
}
// 如果weak ptr要weak引用一个新的obj,则将该obj对应的SideTable取出,赋值给newTable
if (HaveNew) {
// 更改新值指针,获得以 newObj 为索引所存储的值地址
newTable = &SideTables()[newObj];
} else { // 如果weak ptr不需要引用一个新obj,则newTable = nil
newTable = nil;
}
// 加锁操作,防止多线程中竞争冲突
SideTable::lockTwoHaveOld, HaveNew>(oldTable, newTable);
// 避免线程冲突重处理
// location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改,需要返回上边重新处理
if (HaveOld && *location != oldObj) {
SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
goto retry;
}
// 防止弱引用间死锁
// 并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向
if (HaveNew && newObj) {
// 获得新对象的 isa 指针
Class cls = newObj->getIsa();
// 如果cls还没有初始化,先初始化,再尝试设置weak
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized()) {
// 解锁
SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
// 对其 isa 指针进行初始化
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// 如果该类已经完成执行 +initialize 方法是最理想情况
// 如果该类 +initialize 在线程中
// 例如 +initialize 正在调用 storeWeak 方法
// 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记,防止改if分支再次进入
previouslyInitializedClass = cls;
// 重新获取一遍newObj,这时的newObj应该已经初始化过了
goto retry;
}
}
// ② 清除旧值
// 如果之前该指针有弱引用过一个obj那就得需要清除之前的弱引用
if (HaveOld) {
// 如果weak_ptr之前弱引用过别的对象oldObj,则调用weak_unregister_no_lock,在oldObj的weak_entry_t中移除该weak_ptr地址
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// ③ 分配新值
// 如果weak_ptr需要弱引用新的对象newObj
if (HaveNew) {
// (1) 调用weak_register_no_lock方法,将weak ptr的地址记录到newObj对应的weak_entry_t中
// 如果弱引用被释放 weak_register_no_lock 方法返回 nil
newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table,
(id)newObj, location,
CrashIfDeallocating);
// (2) 更新newObj的isa的weakly_referenced bit标志位
if (newObj && !newObj->isTaggedPointer()) {
// 弱引用位初始化操作
// 引用计数那张散列表的weak引用对象的引用计数中标识为weak引用
newObj->setWeaklyReferenced_nolock();
}
// (3)*location 赋值,也就是将weak ptr直接指向了newObj,也就是确保其指针指向是正确的。可以看到,这里并没有将newObj的引用计数+1
*location = (id)newObj;
}
else {
// 没有新值,则无需更改
}
// 解锁,其他线程可以访问oldTable, newTable了
SideTable::unlockTwoHaveOld, HaveNew>(oldTable, newTable);
// 返回newObj,此时的newObj与刚传入时相比,设置了weakly-referenced bit位置1
return (id)newObj;
}
这段代码的作用是更新一个弱引用指向。这里先判断被更新的对象有没有指向旧的对象,要是有,就获取以该旧对象地址为key的在SideTable中的value,然后将这个value放到旧散列表oldTable中。要是没有,就把oldTable置为nil;
然后更新新散列表newTable,如果需要弱引用一个新值,就将newTable的值值为newObj在SideTable中的值,否则将newTable值为nil;
然后是一个避免线程冲突的处理,location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改,需要返回上边重新处理,并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向。
然后清除该指针的旧值:如果之前该指针有弱引用过一个obj那就得需要清除之前的弱引用,如果weak_ptr
之前弱引用过别的对象oldObj,则调用 weak_unregister_no_lock
,在oldObj的weak_entry_t
中移除该weak_ptr地址
然后为该指针分配新值:调用weak_register_no_lock
方法,将weak ptr的地址记录到newObj对应的weak_entry_t
中。如果弱引用被释放 weak_register_no_lock
方法返回 nil。更新newObj的isa的weakly_referenced bit标志位。*location 赋值,也就是将weak ptr直接指向了newObj,也就是确保其指针指向是正确的。可以看到,这里并没有将newObj的引用计数+1
weak_register_no_lock:把新的对象进行注册操作,完成与对应的弱引用表进行绑定操作。
/* weak_table:weak_table_t结构类型的全局的弱引用表。
referent_id:weak指针所指的对象。
*referrer_id:weak修饰的指针的地址。
crashIfDeallocating:如果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash。
*/
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;
// 如果referent为nil 或 referent 采用了TaggedPointer计数方式,直接返回,不做任何操作
if (!referent || referent->isTaggedPointer()) return referent_id;
// 确保被引用的对象可用(没有在析构,同时应该支持weak引用)
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else { //不能被weak引用,直接返回nil
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;
}
}
// now remember it and where it is being stored
// 在 weak_table中找到referent对应的weak_entry,并将referrer加入到weak_entry中
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) { // 如果能找到weak_entry,则讲referrer插入到weak_entry中
append_referrer(entry, referrer); // 将referrer插入到weak_entry_t的引用数组中
}
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为nil或referent采用了TaggedPointer计数方式,直接返回,不做任何操作。
- 如果对象不能被weak引用,直接返回nil。
- 如果对象正在析构,则抛出异常。
- 如果对象没有再析构且可以被weak引用,则调用weak_entry_for_referent方法根据弱引用对象的地址从弱引用表中找到对应的weak_entry,如果能够找到则调用append_referrer方法向其中插入weak指针地址。否则新建一个weak_entry。
Weak的释放
调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?
当释放对象时,其基本流程如下:
1、调用objc_release。用于减少对象的引用计数。当对象的引用计数变为 0 时,对象会被释放。
2、因为对象的引用计数为0,所以执行dealloc。dealloc 是对象的析构函数,用于执行对象释放前的清理工作。
3、在dealloc中,调用了_objc_rootDealloc函数。是根析构函数,执行对象的最终释放操作。
4、在_objc_rootDealloc中,调用了object_dispose函数。用于释放对象的内存并进行相关清理操作。
5、调用objc_destructInstance。用于销毁对象实例,包括释放实例变量。
6、最后调用objc_clear_deallocating。用于处理所有指向该对象的弱引用,将它们清零(nil)。
weak指针管理中使用了3个HashTable!!!
其中两个使用oc对象作为index!!!还有一个使用弱引用指针的地址作为index计算方式
除了全局SideTables(), 其他两个的HashTable都使用开放寻址法作为Hash碰撞解决方法!!!
weak修饰的属性,如何自动置为nil的?
Runtime维护了一个Weak表,用于存储指向某个对象的所有Weak指针。 Weak表其实是一个哈希表,Key是所指对象的地址,Value是Weak指针的地址(这个地址的值是所指对象的地址)的数组。 在对象被回收的时候,经过一层层调用,会最终触发(clearDeallocating)方法将所有Weak指针的值设为nil。
weak 的实现原理可以概括为以下三步:
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。