尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

iOS weak底层原理:从Runtime数据结构到循环引用破解

iOS weak底层原理:从Runtime数据结构到循环引用破解 1. 项目概述为什么iOS的weak如此重要在iOS开发中内存管理是每个开发者都必须跨过的坎。从早期的MRC手动引用计数到现在的ARC自动引用计数苹果一直在努力让开发者从繁琐的内存管理细节中解放出来。但ARC并非魔法它只是编译器在编译时帮我们插入了合适的retain和release调用。而weak关键字则是ARC这套自动化体系中一个至关重要的“安全阀”。想象一下你正在构建一个复杂的视图控制器层级或者设计一个双向引用的数据模型对象之间相互引用形成循环就像两个好朋友紧紧拉住对方的手谁都不肯先松开导致谁都无法被系统释放内存泄漏就此产生。weak的作用就是主动松开一只手打破这个僵局告诉系统“我引用你但我不拥有你你的存亡与我无关。”这听起来简单但其背后的实现机制却精巧而复杂。它不仅仅是一个简单的指针置空操作。系统需要维护一个全局的弱引用表在目标对象被销毁时高效、准确地找到所有指向它的弱引用并将它们安全地置为nil。这个过程必须保证线程安全因为弱引用的设置和清除可能发生在多线程环境下。理解weak的原理不仅能让你在遇到诸如“为什么这个weak变量突然变成了nil”或“使用weak修饰的代理为何能避免循环引用”这类问题时豁然开朗更能让你在编写高性能、高稳定性的代码时做出更明智的架构决策。比如你就能明白为什么IBOutlet属性默认就是weak的以及在某些特定场景下如Block内部使用__weak和__strong的舞蹈是何其重要。接下来我们就深入Runtime的底层拆解这个“安全阀”是如何被锻造出来的。2. weak的核心机制与数据结构设计要理解weak的实现我们必须暂时离开Objective-C的抽象层面潜入其底层基石——Objective-C Runtime。Runtime是用C和汇编编写的动态库为Objective-C提供了面向对象和动态特性的所有支持。weak机制便是Runtime中一个独立且精密的子系统。2.1 全局弱引用表SideTables 与 SideTable系统并非为每一个对象单独维护一个弱引用列表那样效率太低。取而代之的是一种哈希表结构目的是在内存开销、访问速度和并发安全之间取得平衡。这个结构的顶层是一个全局的StripedMapSideTable通常被称为SideTables。你可以把SideTables想象成一个拥有很多抽屉Stripes的柜子。抽屉的数量是固定的比如在iOS上通常是8或64个具体数量经过优化目的是减少哈希冲突。每个抽屉一个SideTable结构都配有一把锁。当一个对象需要被操作时系统会根据该对象的内存地址通过一个哈希函数计算出它应该属于哪个抽屉。这种设计被称为“锁条带化”Lock Striping是一种经典的并发优化手段。它避免了为整个弱引用系统使用一把大锁那样会严重限制并发性能也避免了为每个对象配一把锁那样内存开销巨大。通过将对象分散到多个抽屉不同抽屉的操作可以并行进行只有访问同一个抽屉的对象才需要竞争同一把锁。那么每个抽屉SideTable里面装着什么呢主要包含三样东西自旋锁spinlock_t用于保证对该SideTable操作的线程安全。注意这里历史上使用的是自旋锁它在轻量级、短时间等待的场景下效率很高。但在当前一些系统版本中可能已被更先进的互斥锁如os_unfair_lock所替代但其目的不变——保护数据。引用计数表RefcountMap这是一个哈希表以对象地址为key存储该对象的引用计数。是的对象的引用计数并不直接存储在对象的内存中而是存在这个全局表里。这为一些高级特性如Tagged Pointer腾出了对象内存空间。但这不是我们本次讨论的重点。弱引用表weak_table_t这才是weak机制的核心。它也是一个哈希表负责维护所有弱引用的映射关系。2.2 弱引用表的结构剖析weak_table_t是整个弱引用系统的核心数据库。它的结构设计直接决定了弱引用操作的性能。struct weak_table_t { weak_entry_t *weak_entries; // 一个动态数组存储所有弱引用条目 size_t num_entries; // 当前已存储的条目数量 size_t mask; // 用于哈希计算的掩码通常等于数组容量减一 size_t max_hash_displacement; // 最大哈希冲突探测次数用于优化 };而weak_entry_t则是这个表中的每条记录它代表了一个被弱引用的对象的所有弱引用者信息。struct weak_entry_t { DisguisedPtrobjc_object referent; // 被弱引用的对象地址经过伪装 union { struct { weak_referrer_t *referrers; // 一个动态数组存储所有弱引用指针的地址 uintptr_t out_of_line_ness : 2; // 标记是否使用动态数组 uintptr_t num_refs : PTR_MINUS_2; // 当前弱引用数量 uintptr_t mask; // 数组容量相关掩码 uintptr_t max_hash_displacement; // 最大哈希冲突探测次数 }; struct { // 内联存储当弱引用数量很少4时直接存在这里避免额外内存分配 weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; }; }; };这里的设计非常巧妙DisguisedPtr它是对对象指针的一个封装目的是避免将真实的对象地址作为键值直接存储在表中这在一定程度上可以起到安全混淆的作用。联合体union与内联存储这是性能优化的关键。一个对象可能被很多个weak变量引用也可能只被一两个引用。如果无论多少都动态分配一个数组来存储对于大量只被弱引用一次的对象来说内存碎片和分配开销会很大。因此当弱引用数量不超过4个WEAK_INLINE_COUNT通常是4时这些弱引用指针的地址直接存储在inline_referrers这个固定大小的数组里。只有当弱引用数量超过4个时才会动态分配referrers数组来存储。这种“小对象优化”在系统底层编程中非常常见。一个关键概念weak_entry_t中存储的referrers并不是弱引用变量本身的值而是存储弱引用变量本身的内存地址。例如你有一个weak MyClass *objPtr;这个objPtr变量本身在栈上或堆上有一个地址。weak_entry_t记录的就是这个地址。为什么因为当被引用的对象销毁时系统需要将nil值写入到这个地址中才能将objPtr自动置空。如果只存值就失去了修改它的途径。注意理解“存储弱引用变量的地址”是理解整个weak自动置nil机制的关键。这就像是对象在销毁前有一份所有“观察”它的哨兵weak变量的住址列表。对象“临终”时就派信使去所有这些住址贴上一张写着“nil”的纸条。3. weak的生命周期与操作全流程现在我们结合上述数据结构来看看weak变量从诞生到消亡的完整生命周期以及系统是如何管理它的。3.1 弱引用的创建objc_initWeak当你写下__weak MyClass *weakObj strongObj;时编译器会将其转换为对objc_initWeak函数的调用。这个函数的逻辑如下参数检查首先检查传入的对象指针strongObj是否有效非空、未在释放过程中等。如果是空指针则直接将weakObj赋值为nil并返回。查找或创建 weak_entry_t根据strongObj的地址哈希定位到对应的SideTable和其中的weak_table_t。在weak_table_t中查找是否存在以strongObj为referent的weak_entry_t。如果不存在就创建一个新的weak_entry_t并将其插入到weak_table_t中。初始时由于这是第一个弱引用通常会使用内联存储模式inline_referrers。注册弱引用地址将weakObj变量的内存地址添加到上一步找到或创建的weak_entry_t的引用列表中可能是内联数组也可能是动态数组。关联对象引用计数这里有一个非常重要的步骤但并非直接操作引用计数。weak引用本身不增加对象的retainCount。但是系统会检查对象是否正在进行释放操作通过一个全局的标记。如果对象正在释放则会立即清理其弱引用。这保证了弱引用的创建总是安全的。整个创建过程是在对应SideTable的自旋锁保护下进行的保证了线程安全。3.2 弱引用的使用与读取当你使用这个weak变量时例如MyClass *temp weakObj;编译器会调用objc_loadWeakRetained或其内部等效逻辑。这个过程比看起来要复杂加载并尝试保留函数会从weakObj存储的地址中加载出当前的对象指针。然后它会尝试对这个对象调用objc_retain以增加其引用计数。关键的安全检查在增加引用计数之前和之后Runtime会进行一系列严格的检查以确保对象在“读取”和“保留”这两个动作之间没有被另一个线程销毁。这是一个典型的“TOCTOU”Time-Of-Check-To-Time-Of-Use竞态条件问题。返回对象或nil如果上述步骤成功对象存在且成功增加了引用计数则返回这个有效的对象指针给调用者此时temp是一个强引用会持有该对象。如果对象已被释放则weakObj中存储的已是nil那么整个函数就返回nil。实操心得这正是为什么我们需要在使用weak变量时习惯性地先将其赋值给一个strong局部变量。MyClass *strongObj weakObj;这行代码实际上执行了上述的“加载-检查-保留”原子操作。如果strongObj不为nil那么在当前作用域内这个对象就一定是安全的不会被释放。这避免了在多线程环境下判断if (weakObj)为真后在使用weakObj之前它被另一个线程释放的极端情况。3.3 弱引用的清除对象释放时的 dealloc 路径这是weak机制最精彩的部分——自动置nil。当一个对象的引用计数降为0系统会调用其dealloc方法开始销毁流程。在dealloc的底层实现中会调用_objc_rootDealloc进而触发一系列清理操作其中就包括清除弱引用。调用weak_clear_no_lock在对象内存被真正回收free之前Runtime会调用这个关键函数。它需要对象地址和其对应的SideTable作为参数。查找 weak_entry_t根据对象地址在对应的weak_table_t中查找其weak_entry_t。遍历并置 nil如果找到了对应的weak_entry_t函数会遍历其中记录的所有弱引用变量地址存储在referrers数组或inline_referrers中。对于每一个地址它都将nil值写入到这个地址指向的内存中。这就是所有指向该对象的weak变量瞬间变成nil的魔法时刻。移除条目完成所有弱引用的置空后将这个weak_entry_t从weak_table_t中移除并释放其可能占用的动态数组内存。解锁并继续销毁完成弱引用清理后释放SideTable的锁对象继续执行后续的dealloc操作最终内存被系统回收。这个过程同样是在锁的保护下完成的确保了在对象销毁的瞬间不会有新的弱引用被添加到正在清理的条目中也不会出现漏清理的情况。3.4 弱引用的销毁objc_destroyWeak当weak变量本身生命周期结束时例如局部变量超出作用域或持有weak属性的对象被释放编译器会插入对objc_destroyWeak的调用。这个函数的工作相对简单根据weak变量中存储的最后一次对象地址找到对应的weak_entry_t然后从这个条目的引用列表中移除当前这个weak变量的地址。如果移除后该条目的弱引用数量变为0那么这个weak_entry_t也会被从weak_table_t中移除。4. 高级话题、疑难杂症与最佳实践理解了基本原理我们来看看在实际开发中围绕weak有哪些值得深入探讨的细节和容易踩坑的地方。4.1 __weak 与 __unsafe_unretained 的本质区别两者都表示不增加引用计数的弱引用但行为天差地别__weak如上所述由Runtime自动管理。对象释放时自动置nil是安全的。__unsafe_unretained这是一个“纯指针”赋值。它不参与Runtime的弱引用管理系统。它只是简单记录了对象地址。当对象被释放后这个指针存储的地址不会改变依然指向那块已被回收的内存。这时访问这个指针就会发生野指针访问导致程序崩溃这是未定义行为。为什么还需要__unsafe_unretained兼容性在ARC出现之前代码中大量存在这种赋值。为了兼容性需要这个修饰符。性能极端敏感场景极少数情况下为了避免weak机制带来的微小开销如哈希查找、原子操作在能百分百保证对象生命周期长于弱引用变量的场景下可能会使用它。但99.9%的iOS应用开发都不属于这种场景强烈不建议使用。修饰非Objective-C对象weak只能修饰Objective-C对象。对于Core Foundation对象CFTypeRef或纯C/C对象如果想表达类似“弱引用”的概念只能使用__unsafe_unretained或更底层的桥接和手动管理。注意事项永远将__unsafe_unretained视为“危险”的标志。除非你正在维护一段陈旧的MRC代码或者与特定的Core Foundation API交互并且完全清楚其生命周期否则请坚持使用__weak。4.2 Block 与 weakSelf/strongSelf 舞蹈这是weak使用最频繁也最容易出错的场景之一。循环引用经常发生在Block内部。// 示例典型的循环引用 self.completionHandler ^{ [self doSomething]; // Block 捕获了 self强引用了 self // 而 self 又通过属性强引用了这个 Block形成循环。 };解决方案是使用__weak打破循环__weak typeof(self) weakSelf self; self.completionHandler ^{ [weakSelf doSomething]; };但这引入了新的问题在Block执行过程中weakSelf可能已经变成nil例如在Block被排队执行但还未执行时self被释放了。为了避免在Block执行中途self被释放我们引入了“强弱引用舞蹈”__weak typeof(self) weakSelf self; self.completionHandler ^{ __strong typeof(self) strongSelf weakSelf; // 1. 将弱引用转为强引用 if (!strongSelf) { // 2. 安全检查 return; } [strongSelf doSomething]; // 3. 在Block执行期strongSelf保证了self存活 // ... 可能有多行代码 };步骤解析__strong typeof(self) strongSelf weakSelf;这行代码是关键。它尝试将weakSelf升级为强引用strongSelf。这个操作是原子的如前所述它包含了“读取-检查-保留”的过程。如果此时self还存在则其引用计数会增加确保在后续的Block代码块执行期间self不会被释放。if (!strongSelf) { return; }如果weakSelf已经是nil即self已销毁那么strongSelf也会是nil直接返回避免对空对象操作。之后使用strongSelf来调用方法或访问属性。strongSelf是一个局部强引用它的生命周期仅限于当前Block的执行。当Block执行完毕strongSelf超出作用域强引用消失对象可能被释放。这样就完美地实现了“在需要时持有在执行后释放”的语义。4.3 属性修饰符 weak 与 assign 的区别在声明属性时weak和assign也常常被混淆。weak用于Objective-C对象。特性是对象释放后自动置nil。assign这是默认的修饰符用于标量类型如NSInteger,CGFloat,int,enum等。它也可以用于Objective-C对象但其行为等同于__unsafe_unretained即不进行任何内存管理对象释放后会产生野指针。绝对不要用assign来修饰Objective-C对象属性。对于delegate属性几乎总是使用weak来修饰就是为了避免代理对象通常是视图控制器和被代理对象通常是视图或子控件之间的循环引用。4.4 调试与性能考量调试技巧当怀疑weak变量提前变nil时可以在Xcode的调试控制台使用命令po [object weakReference]来查看对象的弱引用情况需要一定的Runtime知识。使用Instruments的Allocations模板开启“Record Reference Counts”选项可以追踪对象的引用计数变化和所有引用者的历史对于分析循环引用和内存泄漏非常有帮助。性能考量weak变量的读写成本远高于强引用。它涉及哈希表查找、加锁/解锁等操作。虽然对于单次操作来说开销很小但在性能极度敏感的循环例如每秒执行数百万次的紧凑循环中大量访问weak变量可能会成为瓶颈。这也是为什么在Block的“强弱引用舞蹈”中我们一旦获取到strongSelf就在整个Block作用域内使用它而不是反复去访问外部的weakSelf。一个常见的误解有人认为weak变量指向的对象被释放时会触发KVO键值观察或属性观察willSet/didSet。这是错误的。weak的置nil是在Runtime最底层的内存管理层面直接修改指针值它不会触发任何Objective-C级别的消息发送或属性观察机制。它就是一个静默的、原子的内存写入操作。
返回列表