【iOS】weak修饰符

前言

前面我们已经学习了解了sideTable,今天来看看在OC中,sideTable是如何在我们使用weak时工作的。在OC中,weak修饰符是一种用于声明"弱引用"的关键字,其核心特性是不参与对象的引用计数管理,而且当被引用的对象被释放时,weak指针会自动置为nil(避免野指针)。为探究weak的工作原理和底层逻辑,笔者特写此篇来记录对weak的学习。

引用计数与sideTable

OC的内存管理是基于引用计数的(ARC 下由编译器自动生成引用计数增减代码)。对象的isa指针指向其类信息,而对象的内存分布中通常包含:

  • isa指针:指向对象的所属类或元类。
  • 引用计数相关数据:早期直接存储在对象内存头部,后来为了减少内存碎片,现将引用计数和弱引用信息分离到sideTable中。

sideTable是什么

sideTable是一个辅助数据结构(本质是哈希表),用于存储与对象关联的元数据,包括引用计数表、弱引用表和其它元数据(如关联对象、关联引用等)。

引用计数表(RefcountMap):记录对象的引用计数值(分散存储,避免每个对象都占用额外空间)。

弱引用表(WeakMap):记录所有指向该对象的 weak指针地址(键为对象地址,值为 weak指针的集合)。

没个对象可能共享一个sideTable(通过哈希计算映射),因此sideTable的内存开销备份谈到多个对象上。

weak修饰符的底层实现原理

首先,我们来看看调用weak时的底层调用。


weak工作流程第二阶段

我们在调用weak的地方打上断点,然后进行汇编代码调试,然后我们能发现,weak调用了一个objc_storeWeak方法:

然后我们在obj4-906-main源码中找到了这部分方法的底层实现:

这其实是weak的赋值阶段,用于将weak指针的地址注册到目标对象的弱引用表中。

这个函数中的参数含义如下:

  • location:指向 weak变量的指针(即存储 weak指针的内存地址)。例如,有一个 __weak NSObject *obj;,则 &obj就是 location的值。
  • newObj:要赋值给 weak变量的新对象(可能为 nil)。
  • 返回值:返回旧值(即 location原来的对象,若未修改则为 nil)。

返回值中,有三个参数控制 storeWeak函数的行为逻辑:

DoHaveOld:是否处理旧值

若为 true,函数会先检查 location原有的旧值(即之前指向的对象),并从该旧值的弱引用表中移除当前 location地址(避免旧对象释放时错误地清理已失效的 weak指针);

若为 false,则跳过旧值处理(适用于首次赋值或无需清理旧值的场景)。

DoHaveNew:是否处理新值

若为 true,函数会将 newObj的地址注册到其对应的弱引用表中(即把location地址添加到 newObj的弱引用表 referrers数组中);

若为 false,则跳过新值处理(适用于清空 weak指针的场景,如 obj = nil)。

DoCrashIfDeallocating:对象释放时是否崩溃

若为 true,当 newObj正在被释放(deallocating状态)时,函数会触发崩溃(避免向已释放对象注册弱引用);

若为 false,则允许向正在释放的对象注册弱引用(但后续对象释放时会清理该 weak指针)。

storeWeak函数源码:

objective-c 复制代码
storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 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;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            class_initialize(cls, (id)newObj);

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (!_objc_isTaggedPointerOrNil(newObj)) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    // This must be called without the locks held, as it can invoke
    // arbitrary code. In particular, even if _setWeaklyReferenced
    // is not implemented, resolveInstanceMethod: may be, and may
    // call back into the weak reference machinery.
    callSetWeaklyReferenced((id)newObj);

    return (id)newObj;
}

weak工作流程第一阶段

刚刚说objc_storeWeak方法是weak的赋值和持有阶段,这是调用weak的第二阶段,在这之前还有一个声明阶段即初始化阶段。

初始化时,runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。

源码如下:

weak工作流程第三阶段

weak工作流程的第三阶段就是对象释放阶段(清楚弱引用表并置nil):

objc_object::clearDeallocating()函数,它是一个内联函数,内部直接调用了sidetable_clearDeallocating()。这说明clearDeallocating()是对外暴露的接口,而sidetable_clearDeallocating()是具体的实现细节,负责实际的清理操作。

这个函数的主要操作是处理SideTable中的弱引用表和相关元数据。步骤包括:

  1. 获取SideTable:通过SideTables()[this]获取当前对象对应的SideTable实例。
  2. 加锁:使用table.lock()确保线程安全,避免多线程同时修改SideTable导致数据竞争。
  3. 查找引用计数项:在table.refcnts(引用计数表)中查找当前对象的迭代器it。
  4. 检查弱引用标记:如果找到迭代器且其值包含SIDE_TABLE_WEAKLY_REFERENCED标志(表示该对象有弱引用需要处理),则调用weak_clear_no_lock函数清理弱引用表。
  5. 清理引用计数项:从refcnts中删除当前对象的条目。
  6. 解锁:释放SideTable的锁,确保其他线程可以继续操作。

weak的本质

  1. 运行时维护的弱引用跟踪机制

weak的本质是运行时通过SideTable动态跟踪对象与weak指针的关联关系。其核心特性(不参与引用计数、自动置nil)均由以下机制支撑:

  • 不参与引用计数:weak赋值时不调用retain,对象释放时不依赖weak指针的计数。
  • 自动置nil:通过SideTable中的弱引用表,在对象释放时主动遍历并清理所有关联的weak指针地址。
  1. 弱引用表的集中管理

Weak(即__weak修饰的指针)的本质是运行时在SideTable中维护的一张弱引用表(weak_table_t)。该表存储了所有指向当前对象的weak指针地址(referrers数组),是对象释放时定位并清理weak指针的核心依据。

weak置nil

weak指针置nil的关键操作发生在对象释放阶段,由sidetable_clearDeallocating函数触发:

  1. 对象调用dealloc后,执行objc_object::clearDeallocating(内联函数),调用sidetable_clearDeallocating
  2. sidetable_clearDeallocating获取对象的SideTable并加锁,检查引用计数映射(refcnts)中是否存在SIDE_TABLE_WEAKLY_REFERENCED标记(表示被weak引用过)。
  3. 若存在,调用weak_clear_no_lock函数,遍历该对象的弱引用表(weak_table_t.referrers),将每个weak指针的地址(entry->referrers)处的值置为nil(本质是修改内存为0)。
  4. 清理完成后,删除引用计数映射项并解锁SideTable,完成weak指针的置nil操作。

总结

weak的实现以SideTable和弱引用表为核心,通过运行时动态跟踪对象与weak指针的关联关系,在对象释放时主动清理所有weak指针并置nil,既避免了循环引用导致的内存泄漏,又保证了内存访问的安全性,其本质是运行时维护的弱引用跟踪机制。

相关推荐
谈吐大方的鹏sir13 小时前
SwiftUI中的状态管理
ios
0wioiw016 小时前
Apple基础(Xcode①-项目结构解析)
ide·macos·xcode
烈焰晴天19 小时前
一款基于 ReactNative 最新发布的`Android/iOS` 新架构文档预览开源库
android·react native·ios
2501_9159090619 小时前
iOS电池寿命与App能耗监测实战 构建完整性能监控系统
android·ios·小程序·https·uni-app·iphone·webview
ghostwritten20 小时前
在 Mac 上用 Vagrant 安装 K8s
macos·kubernetes·vagrant
不自律的笨鸟1 天前
iPhone 神级功能,3D Touch 回归!!!
ios·手机·iphone
胡萝卜大王驾到通通闪开1 天前
Mac m系列芯片安装node14版本使用nvm + Rosetta 2
macos
java叶新东老师1 天前
ubuntu资源共享samba 安装与配置 mac/windows共享ubuntu文件资源
windows·ubuntu·macos
Magnetic_h2 天前
【iOS】类和分类的加载过程
笔记·学习·ios·objective-c·xcode