当前位置: 首页 > news >正文

【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函数源码:

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,既避免了循环引用导致的内存泄漏,又保证了内存访问的安全性,其本质是运行时维护的弱引用跟踪机制。

http://www.xdnf.cn/news/1213363.html

相关文章:

  • USRP捕获手机/路由器数据传输信号波形
  • 国内好用的智能三防手机,适合户外、工业、公共安全等场景
  • LLMs之Agent:GLM-4.5的简介、安装和使用方法、案例应用之详细攻略
  • 【MySQL学习|黑马笔记|Day3】多表查询(多表关系、内连接、外连接、自连接、联合查询、子查询),事务(简介、操作、四大体系、并发事务问题、事务隔离级别)
  • 智能车辆热管理测试方案——提升效能与保障安全
  • Three.js 与 WebXR:初识 VR/AR 开发
  • 多模通信·数据采集:AORO P9000U三防平板带来定制化解决方案
  • 如何在出售Windows11/10/8/7前彻底清除电脑数据
  • B站 XMCVE Pwn入门课程学习笔记(6)
  • 洛谷刷题7.30
  • C++反射
  • 认识ansible(入门)
  • Javascript 基础总结
  • docker:将cas、tomcat、字体统一打包成docker容器
  • VS Code中如何关闭Github Copilot
  • 技术速递|GitHub Copilot 的 Agent 模式现已全面上线 JetBrains、Eclipse 和 Xcode!
  • 企业级WEB应用服务器TOMCAT
  • 【IDEA】JavaWeb自定义servlet模板
  • 工厂方法模式:从基础到C++实现
  • 华为昇腾NPU卡 文生视频[T2V]大模型WAN2.1模型推理使用
  • Kubernetes资源调优终极指南:从P95识别到精准配置
  • Kong API Gateway的十年进化史
  • Spring Cloud Gateway静态路由实战:Maven多模块高效配置指南
  • ‌CASE WHEN THEN ELSE END‌
  • YOLO-01目标检测基础
  • 【Rust多进程】征服CPU的艺术:Rust多进程实战指南
  • 力扣热题100-------74.搜索二维矩阵
  • SpringBoot 整合 自定义MongoDB
  • Flutter封装模板及最佳实践
  • CVAE 回顾版