「OC」内存管理——weak
文章目录
weak的使用场景
我们在使用ARC的时候,仍然会遇到循环引用的问题,例如持有协议对象,两个对象的相互引用,这时候就需要使用weak。
核心特性:
- 不增加引用计数: 声明为
weak
的指针不会增加其所指向对象的retainCount
。 - 自动置零 (Zeroing): 这是最关键的特性!当
weak
指针所指向的对象被释放(引用计数降为0,调用dealloc
)时,所有指向该对象的weak
指针会被自动设置为nil
。这彻底避免了“野指针”访问崩溃的风险。 - 可选性: 因为可能为
nil
,访问weak
变量时需要谨慎(检查是否存在)。
weak的实现原理
我们使用__weak
修饰OC对象,我们
NSObject *obj = [[NSObject alloc] init];
__weak NSObject *weak = obj;
我们会发现需要步入objc_initWeak
,经过简单判空之后,进入storeWeak
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
我们进入storeWeak
函数
template <HaveOld haveOld, HaveNew haveNew,
enum CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
// 断言确保至少有一个操作: 要么有旧值需要清理,要么有新值需要设置
ASSERT(haveOld || haveNew);
// 如果没有新值,newObj必须为nil
if (!haveNew) ASSERT(newObj == nil);
// 初始化变量
Class previouslyInitializedClass = nil; // 记录已经初始化的类
id oldObj; // 指向当前位置的旧对象
SideTable *oldTable; // 旧对象所在的SideTable
SideTable *newTable; // 新对象所在的SideTable
// 获取新旧对象的锁,按照锁地址排序避免死锁
// 如果旧值在获取锁的过程中发生了变化则重试
retry:
if (haveOld) {
// 获取当前位置指向的旧对象
oldObj = *location;
// 获取旧对象的SideTable
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
// 获取新对象的SideTable
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 同时锁定新旧对象的SideTable(根据haveOld/haveNew条件编译)
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
// 检查在获取锁期间旧值是否发生变化(可能被其他线程修改)
if (haveOld && *location != oldObj) {
// 解锁后重试整个操作
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// 防止弱引用机制和+initialize方法之间发生死锁(就是在+initialize之中使用weak修饰符)
// 确保弱引用对象的isa已经完成初始化,
if (haveNew && newObj) {
// 获取新对象的类
Class cls = newObj->getIsa();
// 检查类是否已经初始化(避免未初始化类导致的死锁)
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
// 如果类未初始化,解锁并触发类的+initialize方法
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// 调用runtime的类初始化方法
class_initialize(cls, (id)newObj);
// 记录已经初始化过的类
previouslyInitializedClass = cls;
// 重新尝试整个操作
goto retry;
}
}
// 清理旧值(如果存在)
if (haveOld) {
// 从旧对象的weak_table中注销当前指针位置的弱引用
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// 设置新值(如果存在)
if (haveNew) {
// 注册新的弱引用到新对象的weak_table
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock可能返回nil(如果对象正在释放)
// 如果不是tagged pointer或nil对象
if (!_objc_isTaggedPointerOrNil(newObj)) {
// 设置对象的weakly_referenced标志位
newObj->setWeaklyReferenced_nolock();
}
// 更新location指向的位置为新对象
// 注意:不能在其它地方设置*location,否则可能引入竞态条件
*location = (id)newObj;
}
else {
// 没有新值,不需要更新存储位置
}
// 释放新旧SideTable的锁
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// 必须在无锁状态下调用(可能触发任意代码)
// 特别要注意:即使_setWeaklyReferenced未实现,resolveInstanceMethod可能被调用
// 并可能回调到弱引用机制中
callSetWeaklyReferenced((id)newObj);
// 返回实际设置的对象(可能因对象正在释放而不同)
return (id)newObj;
}
我们可以看到通过storeWeak
实际上是接收了5个参数,分别是haveOld
、haveNew
、crashIfDeallocating
、location
、
newobj
。大概进行两个处理,一个是对旧值的释放,一个是让指针指向新的引用
- 如果是对旧指针的移除,那么就会进入
weak_unregister_no_lock
方法 - 如果是指向新的引用,就会进入
weak_register_no_lock
,把这个新的weak对象添加到引用表之中
SideTable
SideTable
又称为散列表,是用来存储没有指针优化的OC对象,或者是存储extra_rc
sideTable的操作
1、如果不是Nonpointer_isa
,则直接操作SideTables
散列表,此时的散列表并不是只有一张,而是有很多张
2、判断是否正在释放
,如果正在释放,则执行dealloc流程
3、执行extra_rc+1
,即引用计数+1操作,并给一个引用计数的状态标识carry
,用于表示extra_rc
是否满了
4、如果carray
的状态表示extra_rc的引用计数满
了,此时需要操作散列表
,即 将满状态的一半拿出来存到extra_rc
,另一半存在 散列表的rc_half
。这么做的原因是因为如果都存储在散列表,每次对散列表操作都需要开解锁,操作耗时,消耗性能大,这么对半分
操作的目的在于提高性能
struct 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);
};
SideTable
并不是全局唯一的,如果散列表只有一张表
,意味着全局所有的对象都会存储在一张表中,都会进行开锁解锁(锁是锁整个表的读写)。当开锁时,由于所有数据都在一张表,则意味着数据不安全
weak_table_t
/**
* 弱引用表(Weak Reference Table)核心数据结构
* 用于管理对象的弱引用关系
*/
struct weak_table_t {
weak_entry_t *weak_entries; // 指向弱引用条目数组的数组指针(哈希表)
size_t num_entries; // 当前表中的有效条目数量
uintptr_t mask; // 哈希表容量掩码 = 容量 - 1(用于索引计算)
uintptr_t max_hash_displacement; // 最大哈希冲突偏移量(用于哈希表性能优化)
};
/**
* 弱引用注册时的对象释放处理选项
* 控制当对象正在释放时如何处理弱引用注册
*/
enum WeakRegisterDeallocatingOptions {
ReturnNilIfDeallocating, // 如果对象正在释放,返回nil
CrashIfDeallocating, // 如果对象正在释放,触发崩溃
DontCheckDeallocating // 不检查对象是否正在释放(内部使用)
};
/**
* 向弱引用表中注册一个新的弱引用关系
*
* @param weak_table 目标弱引用表
* @param referent 被弱引用的目标对象
* @param referrer 弱引用指针的地址(如 &weakVar)
* @param deallocatingOptions 对象正在释放时的处理选项
* @return 当对象有效时返回对象本身,否则根据选项返回nil或触发崩溃
*/
id weak_register_no_lock(weak_table_t *weak_table, id referent,
id *referrer, WeakRegisterDeallocatingOptions deallocatingOptions);
/**
* 从弱引用表中注销一个弱引用关系
*
* @param weak_table 目标弱引用表
* @param referent 被弱引用的目标对象
* @param referrer 弱引用指针的地址(如 &weakVar)
*/
void weak_unregister_no_lock(weak_table_t *weak_table, id referent, id *referrer);
#if DEBUG
/**
* 调试函数:检查对象是否被弱引用
*
* @param weak_table 目标弱引用表
* @param referent 要检查的对象
* @return 如果对象有弱引用则返回true,否则false
*/
bool weak_is_registered_no_lock(weak_table_t *weak_table, id referent);
#endif
/**
* 对象销毁时清除所有弱引用
* - 将指向该对象的所有弱引用指针设为nil
* - 从弱引用表中移除该对象的条目
*
* @param weak_table 目标弱引用表
* @param referent 被销毁的对象
*/
void weak_clear_no_lock(weak_table_t *weak_table, id referent);
__END_DECLS // 结束C语言链接规范
#endif /* _OBJC_WEAK_H_ */ // 头文件结束
weak_entry_t
struct weak_entry_t {
// 1. 被弱引用的目标对象 (共同目标)
DisguisedPtr<objc_object> referent; // 伪装的指针
// 2. 双模式存储结构:联合体优化内存使用
union {
// 模式1: 动态哈希表 (用于存储大量弱引用)
struct {
weak_referrer_t *referrers; // 指向存储弱引用地址的数组
uintptr_t out_of_line_ness : 2; // 存储模式标志位
uintptr_t num_refs : 62; // 当前存储的弱引用数量
uintptr_t mask; // 哈希表容量掩码
uintptr_t max_hash_displacement; // 最大哈希冲突偏移量
};
// 模式2: 内联数组 (小数据优化)
struct {
// 内联存储槽 (WEAK_INLINE_COUNT=4)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
// 判断当前是否使用动态数组存储方式
bool out_of_line() {
// 通过检查out_of_line_ness标志位是否为REFERRERS_OUT_OF_LINE(一般为1)来判断
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;
// 内联数组其余位置置为nil
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
weak_unregister_no_lock
移除旧指针的源码如下,我们传入的参数为,弱应用表的指针,weak指针,weak指针地址
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;
// 在弱引用表中查找该对象对应的条目(weak_entry_t)
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 从该条目中移除指定的弱引用指针(referrer)
remove_referrer(entry, referrer);
// 检查移除后该条目是否为空(即是否已经没有指向该对象的弱引用)
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
// 如果当前是动态哈希表模式,并且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);
}
}
// 注意:这里并没有设置 *referrer = nil。
// 这是因为 objc_storeWeak() 要求:在解除注册期间,弱引用变量的值不能改变。
// 这个函数只负责从弱引用表中移除关联,调用者会在适当的时候将弱引用变量置为 nil。
}
-
首先,它会通过weak_entry_for_referent方法在weak_table中找出referent对应的weak_entry_t
-
如果找到的话,在通过remove_referrer方法在weak_entry_t中移除referrer
-
移除元素后,判断此时weak_entry_t中是否还有元素 (empty==true?)
-
如果此时weak_entry_t已经没有元素了,则需要将weak_entry_t从weak_table中移除
注意:我们调用
remove_referrer
方法,但是没有将referrer置空,是将置空的操作在返回上级完成
weak_register_no_lock
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions)
{
// 1. 将传入的泛型id转换为具体类型
objc_object *referent = (objc_object *)referent_id; // 被弱引用的对象
objc_object **referrer = (objc_object **)referrer_id; // 弱引用指针的地址(如 &weakVar)
// 2. 处理Tagged Pointer和nil情况:这些特殊对象不需要弱引用管理
if (_objc_isTaggedPointerOrNil(referent)) return referent_id;
// 3. 确保被引用对象是有效的(未在释放过程中)
if (deallocatingOptions == ReturnNilIfDeallocating ||
deallocatingOptions == CrashIfDeallocating) {
bool deallocating; // 标记对象是否正在释放
// 检查对象是否实现了自定义的引用计数方法
if (!referent->ISA()->hasCustomRR()) {
// 使用默认的释放状态检查
deallocating = referent->rootIsDeallocating();
}
else {
// 动态查找对象是否实现了 allowsWeakReference 方法
// 注意:这里直接调用查找方法是为了避免加锁问题
auto allowsWeakReference = (BOOL(*)(objc_object *, SEL))
lookUpImpOrForwardTryCache((id)referent, @selector(allowsWeakReference),
referent->getIsa());
// 如果对象不支持弱引用(没有实现该方法或已转发)
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil; // 返回nil表示注册失败
}
// 调用对象的 allowsWeakReference 方法
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; // 安全返回nil
}
}
}
// 4. 开始弱引用注册流程
weak_entry_t *entry;
// 5. 查找该对象是否已存在弱引用条目
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 5.1 若条目存在:将新弱引用添加到现有条目中
append_referrer(entry, referrer);
}
else {
// 5.2 若不存在:创建新条目
weak_entry_t new_entry(referent, referrer);
// 5.3 检查并扩容弱引用表(如果需要)
weak_grow_maybe(weak_table);
// 5.4 将新条目插入到弱引用表中
weak_entry_insert(weak_table, &new_entry);
}
// 6. 重要说明:不在此处修改 *referrer 的值
// objc_storeWeak() 要求值不能在此改变
return referent_id; // 返回原始对象
}
weak的结构
对象的释放
clearDeallocating
当我们调用rootDealloc
对对象进行释放,如果我们没有使用到sidetable
那么对象可以进行快速释放,但我们主要还是探究当有sideTable
时的调用流程。当我们使用sideTable
时,我们就会调用object_dispose
->clearDeallocating
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());
}
我们可以看到这个函数有两个分支,不难知道,当我们的isa指针没有开启nonpointer优化时,那就直接清空这个对象在sidetable上的所有引用计数,另一个就是对优化的isa进行判断,是否使用sidetable进行辅助应用技术或者有weak修饰的
clearDeallocating_slow
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
// 断言:确保当前对象满足执行慢速路径的条件:
// 1. 使用非指针型isa(nonpointer ISA,包含额外信息位)
// 2. 对象有弱引用和/或引用计数存储于sidetable
ASSERT(isa().nonpointer && (isa().weakly_referenced
#if ISA_HAS_INLINE_RC
|| isa().has_sidetable_rc
#endif
));
// 获取当前对象关联的SideTable:
// 1. SideTables是全局哈希表,根据对象地址分组管理
// 2. 此操作完成对象指针->对应SideTable的映射
SideTable& table = SideTables()[this];
// 加锁:对SideTable进行线程安全保护
// 防止多线程同时操作弱引用表和引用计数表
table.lock();
// 处理弱引用:
// 如果对象有弱引用(isa.weakly_referenced标志位为1)
if (isa().weakly_referenced) {
// 清理弱引用表:
// 1. 扫描weak_table将所有指向当前对象的weak指针置为nil
// 2. 移除当前对象在weak_table中的所有条目
// 3. no_lock前缀表示调用者已持有锁
weak_clear_no_lock(&table.weak_table, (id)this);
}
// 处理引用计数:
// 只在支持内联引用计数的架构上检查该条件
#if ISA_HAS_INLINE_RC
// 检查sidetable中是否存储了额外引用计数
if (isa().has_sidetable_rc) {
#endif
// 从refcnts哈希表中安全移除当前对象:
// 1. 删除对象对应的引用计数条目
// 2. 此操作必须与弱引用清理在同一个锁保护下
table.refcnts.erase(this);
#if ISA_HAS_INLINE_RC
}
#endif
// 解锁:释放SideTable锁
// 允许其他线程操作此SideTable
table.unlock();
}
weak_clear_no_lock
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
// 将传入的弱引用目标对象转为objc_object指针类型
objc_object *referent = (objc_object *)referent_id;
// 在弱引用表中查找目标对象对应的条目(weak_entry_t)
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
// 如果找不到对应条目(正常情况下不应该发生),直接返回
if (entry == nil) {
/// XXX 本不该发生,但在CF与objc混用不匹配时可能发生
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// 准备清理弱引用指针
weak_referrer_t *referrers; // 指向weak指针地址数组的指针
size_t count; // 数组长度
// 判断entry存储方式:外联(out-of-line)存储或内联(inline)存储
if (entry->out_of_line()) {
// 外联存储:referrers指向动态分配的数组
referrers = entry->referrers;
count = TABLE_SIZE(entry); // 获取实际数组大小
}
else {
// 内联存储:referrers指向内联缓冲区
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT; // 固定大小(通常为4)
}
// 遍历所有weak指针地址
for (size_t i = 0; i < count; ++i) {
// 获取存储weak指针的地址(二级指针)
objc_object **referrer = referrers[i];
if (referrer) { // 确保指针地址有效
// 情况1:weak指针正确指向目标对象
if (*referrer == referent) {
// 核心操作:将weak指针置为nil!
*referrer = nil; // 这就是weak自动置nil的实现
}
// 情况2:非预期的指针值(可能发生逻辑错误)
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_table中移除该entry
weak_entry_remove(weak_table, entry);
}