问题的答案。
Objective-C依赖手动的retain/release周期和直接指针来实现弱引用,这需要运行时调换或全局哈希表,这在每次对象访问时都会产生显著的性能损失。当Apple设计Swift时,他们要求一种自动内存管理模型,支持零化弱引用——在引用的对象被解除分配时自动变为nil——而不对大多数从不遇到弱引用的对象施加负担。这一需求导致了侧表架构的开发,仅在需要时外部化弱引用元数据。
核心问题在于平衡内存效率与安全性。如果每个对象头部都包含内联存储来跟踪弱引用(例如,弱指针的链表或内联弱计数),那么每个类实例的内存占用将显著增加,惩罚那些仅使用强引用的性能关键代码。相反,将弱引用存储在一个按对象地址键入的全局哈希表中会引入同步瓶颈和复杂的回收逻辑。当对象解除分配时,挑战在于创建一个对缺乏弱引用的对象不产生成本的机制,同时保证在最后一个强引用消失时进行线程安全的原子零化。
Swift采用了一种侧表系统,每个类实例头部包含一个可空指针,指向一个单独的堆分配的侧表结构。这个侧表存储弱引用计数和一个指向对象的反向指针;弱引用实际上指向这个侧表,而不是对象本身。当强引用计数降为零时,运行时会原子性地将侧表中的对象指针设置为nil,导致所有现存的弱引用在下次访问时观察到nil,而对象的内存保持分配状态直到弱引用计数也降为零,此时侧表和对象内存都被回收。
生活中的情况
想象一下开发一个高分辨率图像管道的社交媒体应用程序,其中ViewController实例下载和显示用户头像。为了防止冗余的网络请求,您实现了一个ImageCache单例,它存储对下载的UIImage对象的引用,以便多个显示相同头像的视图控制器可以共享底层内存缓冲区。
考虑的一个方法是在NSCache中存储强引用,采用任意的驱逐策略。这保证了立即访问和类型安全,但导致严重的内存泄漏,因为缓存无限期保留每个图像,最终在长时间滚动会话中触发内存警告和应用终止。优点包括简单和快速访问,但无限的内存增长使其不适合生产环境。
另一种考虑的方法是实现手动观察者模式,其中视图控制器在解除分配时通知缓存以使用委托协议删除特定条目。虽然这在理论上防止了泄漏,但它引入了视图层与缓存层之间的脆弱紧耦合,需要大量的样板代码来处理快速导航过渡期间的竞争条件,并在通知消息被遗漏或延迟时存在崩溃的风险。
所选择的解决方案利用了Swift在缓存实现中的本地弱引用:
class ImageCache { private var cache: [URL: WeakBox<UIImage>] = [:] func image(for url: URL) -> UIImage? { return cache[url]?.value } func setImage(_ image: UIImage, for url: URL) { cache[url] = WeakBox(value: image) } } final class WeakBox<T: AnyObject> { weak var value: T? init(value: T) { self.value = value } }
通过将缓存字典的值声明为通过WeakBox包装的弱引用,ImageCache可以在返回之前验证图像是否仍然存在于内存中,同时在没有任何视图控制器主动显示该头像时允许自动回收。这消除了内存泄漏和手动记账的开销,在快速滚动信息流期间使峰值内存使用减少了40%,并防止了系统的内存监视程序终止应用。
候选人常常忽略的内容
为什么访问弱引用可能比访问强引用要慢,并且在什么具体条件下这种性能差异变得可测?
访问弱引用需要解除对象头部中存储的侧表指针,然后从该侧表中执行原子加载对象指针以检查它是否已被置零。虽然开销最小(通常是一个额外的间接引用),但在迭代大量集合(数千个项目)且每个元素在紧循环中通过弱引用进行访问时,它会变得可测,而强引用只需一次指针追踪而没有原子保证。
什么在实现层面区分无主引用和弱引用,并且为什么尝试在对象解除分配后访问无主引用会触发运行时崩溃而不会返回nil?
与利用侧表来启用零化的弱引用不同,无主引用(在默认安全模式下)也引用侧表,但假设只要无主引用存在,对象会保持分配状态,如果对象被解除分配则会崩溃,因为侧表条目被标记为销毁但未置为nil。候选人常常忽略的是,不安全的无主引用完全绕过侧表,在解除分配后访问时行为类似悬挂的C指针,当被访问时会破坏内存,而安全的无主引用至少通过侧表的已解除分配位进行确定性捕获。
为什么对象实例的内存在deinit完成后及所有强引用消失后仍然保留在堆中,并且当这块内存实际释放时?
内存保持存在是因为侧表维护了一个弱引用计数;对象头部及其相关存储无法被回收,直到弱计数降为零,确保弱引用永远不会指向回收的内存。只有在最后一个弱引用被销毁(将弱计数降至零)后,运行时才会释放侧表和对象的内存区域,这是一个开发人员看不到的过程,但对于防止使用后的释放漏洞至关重要。