8-11.【内存管理机制】在闭包中使用 unowned/self 和 weak/self 的区别和风险?

32 阅读6分钟

在 Swift 闭包捕获列表中使用 [unowned self][weak self],本质上是在性能风险安全性之间做抉择。

虽然它们都能打破循环强引用(Retain Cycle),但底层的处理逻辑和失败后果完全不同。


1. 核心区别对比

特性[weak self][unowned self]
捕获类型变成可选型 (Controller?)保持非可选型 (Controller)
自动置 nilself 释放后,闭包内访问得到 nilself 释放后,指针地址依然残留
崩溃风险零风险。配合 guard let 安全处理高风险。若 self 已释放,访问即 崩溃
底层开销较高。需操作侧表 (Side Table) 并进行可选解包较低。直接访问内存,类似于强引用性能
语义假设“我不确定执行时你还在不在。”“我确定执行时你一定还在。”

2. 详细分析与风险

[weak self]:安全但略显繁琐

当闭包被捕获时,它不增加 self 的引用计数。

  • 执行逻辑:闭包执行时,会尝试将 weak 引用提升为强引用。如果 self 已经被释放,提升失败,返回 nil

  • 代码风格:通常需要手动解包。

    Swift

    service.fetchData { [weak self] in
        guard let self = self else { return } // 安全守卫
        self.updateUI()
    }
    
  • 风险:几乎没有内存层面的风险。唯一的“风险”是逻辑中断——如果闭包里的代码非常重要(如数据同步),而此时对象已销毁,后续逻辑将不再执行。

[unowned self]:高效但伴随悬垂指针

unowned 就像是在没有安全网的情况下高空作业。

  • 执行逻辑:它假定对象在闭包调用期间始终存活。访问时,运行时会检查 Unowned 计数。

  • 风险:运行时崩溃。如果闭包是一个异步回调(例如网络请求),而用户在请求返回前关闭了页面(对象已 deinit),此时闭包触发 unowned self 访问会直接抛出 fatal error

    Swift

    service.fetchData { [unowned self] in
        // 如果 self 已经销毁,这里直接 Crash!
        self.updateUI() 
    }
    
  • 底层细节unowned 会让对象进入“僵尸状态”——虽然 deinit 执行了,但对象的物理内存在无主引用计数归零前不会被完全回收。


3. 场景选择指南

weak self 的情况(推荐 90% 的场景):

  • 所有异步回调:网络请求、定时器(Timer)、复杂的 GCD 调度。
  • 生命周期不明确:你不确定闭包什么时候被触发,也不确定触发时对象是否还在。

unowned self 的情况:

  • 确定闭包与对象生命周期一致:例如,一个 View 内部的动画闭包,或者闭包本身就是对象初始化的一部分且不会逸出。
  • 父子关系极其明确:例如,一个对象持有一个闭包,而这个闭包只会被该对象自己调用,且在对象销毁前一定会停止。

vs [unowned self] memory lifecycle diagram showing object deallocation and potential crash point]


4. 一个容易被忽略的风险:[weak self] 的提前释放

有时候你会发现,使用 [weak self] 导致闭包里的代码压根没跑完。

  1. 网络请求发起,self(页面)被捕获。
  2. 用户点击“返回”,页面销毁,强引用归零。
  3. 网络返回,闭包执行,guard let self = self 失败,直接 return

如果闭包里有必须执行的清理逻辑(如写入磁盘、断开连接),使用 weak self 可能会导致这些逻辑被静默跳过。 此时应该审视:这个逻辑是否真的应该放在该对象的闭包里,还是应该剥离到专门的 Data Manager 中。


总结建议

  • 新手/普通业务:无脑使用 [weak self]。多写一行 guard let 换取整个 App 的稳定性是绝对值得的。
  • 追求性能/确定性关系:在确定闭包执行期间对象绝对存活的场景(如同步执行的 forEach 闭包,虽然这种场景通常不需要捕获列表),可以使用 unowned

英文版

8-11. [Memory Management] What are the differences and risks of using unowned self and weak self in closures?

Using [unowned self] or [weak self] in a Swift capture list is essentially a choice between performance/risk and safety.

While both break strong reference cycles (Retain Cycles), their underlying logic and the consequences of failure are entirely different.


1. Core Comparison

Feature[weak self][unowned self]
Captured TypeBecomes an Optional (Controller?)Remains Non-optional (Controller)
Auto-nil-ingYes. If self is deallocated, access returns nil.No. If self is deallocated, the pointer address remains.
Crash RiskZero Risk. Use guard let for safe handling.High Risk. Accessing a deallocated self causes a Crash.
OverheadHigher. Requires Side Table access and optional unwrapping.Lower. Direct memory access similar to strong references.
Semantic Assumption"I'm not sure if you'll still be around when I execute.""I'm certain you'll still be around when I execute."

2. Detailed Analysis and Risks

[weak self]: Safe but Verbose

When a closure captures self weakly, it does not increment the reference count.

  • Execution Logic: When the closure runs, it attempts to "promote" the weak reference to a strong one. If self has already been deallocated, the promotion fails and returns nil.

  • Coding Style: Usually requires manual unwrapping.

    Swift

    service.fetchData { [weak self] in
        guard let self = self else { return } // Safety guard
        self.updateUI()
    }
    
  • Risk: Almost zero risk at the memory level. The only "risk" is logic interruption—if the code inside the closure is critical (e.g., data synchronization) and the object is destroyed, the subsequent logic will simply not run.

[unowned self]: Efficient but Dangerous

unowned is like working at heights without a safety net.

  • Execution Logic: It assumes the object is always alive during the closure's execution. Upon access, the runtime checks the Unowned reference count.

  • The Risk: Runtime Crash. If the closure is an asynchronous callback (e.g., a network request) and the user closes the page (the object deinits) before the request returns, calling unowned self will trigger a fatal error.

    Swift

    service.fetchData { [unowned self] in
        // If self is already destroyed, this CRASHES immediately!
        self.updateUI() 
    }
    
  • Low-level Detail: unowned keeps the object in a "Zombie state"—even though deinit has run, the physical memory is not fully reclaimed until the unowned reference count also reaches zero.


3. Usage Guidelines

When to choose weak self (Recommended for 90% of cases):

  • All Asynchronous Callbacks: Network requests, Timers, or complex GCD scheduling.
  • Unclear Lifecycles: You aren't sure when the closure will be triggered or if the object will still exist at that time.

When to choose unowned self:

  • Lifecycles are Identical: For example, an animation closure inside a View, or a closure that is part of the object's initialization and does not escape.
  • Explicit Parent-Child Relationship: For instance, an object holds a closure that will only be called by itself and is guaranteed to stop before the object is destroyed.

4. A Hidden Risk: Premature Exit with [weak self]

Sometimes, using [weak self] causes critical code to simply fail to complete:

  1. A network request starts; self (the page) is captured.
  2. The user taps "Back"; the page is destroyed, and the strong count hits zero.
  3. The network returns; the closure executes; guard let self = self fails and returns immediately.

If the closure contains logic that must execute (e.g., saving to disk, disconnecting a socket), using weak self might cause these steps to be silently skipped. In such cases, you should re-evaluate your architecture: should this logic live in the object's closure, or should it be moved to a dedicated Data Manager?


Summary Advice

  • Beginners / General Business Logic: Use [weak self] by default. Adding one line of guard let to ensure app stability is always worth it.
  • Performance Tuning / Guaranteed Relationships: Use unowned only when you are certain the object is alive during execution (e.g., a synchronous forEach closure, though capture lists are usually unnecessary there).