「OC」内存管理——weak

「OC」内存管理——weak

weak的使用场景

我们在使用ARC的时候,仍然会遇到循环引用的问题,例如持有协议对象,两个对象的相互引用,这时候就需要使用weak。

核心特性:

  1. 不增加引用计数: 声明为 weak 的指针不会增加其所指向对象的 retainCount
  2. 自动置零 (Zeroing): 这是最关键的特性!当 weak 指针所指向的对象被释放(引用计数降为0,调用 dealloc)时,所有指向该对象的 weak 指针会被自动设置为 nil。这彻底避免了“野指针”访问崩溃的风险。
  3. 可选性: 因为可能为 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个参数,分别是haveOldhaveNewcrashIfDeallocatinglocation

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。
}
  1. 首先,它会通过weak_entry_for_referent方法在weak_table中找出referent对应的weak_entry_t

  2. 如果找到的话,在通过remove_referrer方法在weak_entry_t中移除referrer

  3. 移除元素后,判断此时weak_entry_t中是否还有元素 (empty==true?)

  4. 如果此时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的结构

img

对象的释放

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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值