Swift 中 unowned self 的隐晦陷阱:为什么“无主引用”可能毁掉你的 App

21 阅读3分钟

若你只想记住一句话:“当闭包生命周期可能长于 self 时,永远不要使用 unowned。”

从一段崩溃代码说起

// 看似无害的「定时器」
class HomeViewController: UIViewController {
    private var timer: Timer?
    override func viewDidLoad() {
        super.viewDidLoad()
        // ⚠️ 崩溃隐患:unowned self
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [unowned self] _ in
            self.updateUI()   // 若 1s 内用户退出页面 → 野指针崩溃
        }
    }
    deinit { print("HomeViewController 已释放") }
    private func updateUI() { /* ... */ }
}

运行步骤:

  1. 用户进入页面 → Timer 持有闭包 → 闭包持有 unowned self。
  2. 用户在 0.8s 时点击返回 → HomeViewController 被释放 → 内存地址变野。
  3. 1s 到时系统再回调闭包 → 访问野指针 → EXC_BAD_ACCESS

unowned 与 weak 的底层区别

关键字运行时结构体引用计数变化对象销毁后访问结果开销
weakWeakReference不 +1自动置 nil
unownedUnmanaged不 +1野指针(非 nil

Swift 源码层面(swift/stdlib/public/core/HeapObject.cpp):

  • weak 会被登记到 side-table,对象销毁时所有 weak 指针被批量置 nil。
  • unowned 仅保存原始地址,销毁时 不做任何后置清理,访问时通过 swift_unknownObjectUnownedTakeStrong 尝试“复活”对象;若失败则直接崩溃。

官方文档没说清楚的 3 个场景

  1. 动画/网络回调
// UIView.animate 的逃逸闭包
func flyEmoji() {
    UIView.animate(withDuration: 2, animations: { [unowned self] in
        self.emojiView.alpha = 0
    })        // 若 2s 内用户退出页面 → 崩溃
}
  1. Combine / RxSwift
// Combine 订阅
cancellable = publisher
    .sink { [unowned self] value in
        self.handle(value)   // 上游发值时 self 可能已死
    }
  1. Swift Concurrency
// Task 闭包
Task { [unowned self] in
    await self.loadData()    // 任务未结束时 self 被释放 → 崩溃
}

工程级“逃生舱” checklist

已将您提供的场景与推荐写法整理为 Markdown 表格:

场景推荐写法备注
定时器[weak self] + timer.invalidate() on deinit双重保险
动画[weak self] + 判断 self?.viewIfLoaded != nil避免操作已卸载视图
Combine[weak self] + cancellable.cancel() on deinit及时取消订阅
SwiftUI直接使用 struct + @State无引用循环问题

扩展:用“WeakBox” 消灭可选链

/// 让 weak 引用也能像 unowned 一样方便,但更安全
final class WeakBox<T: AnyObject> {
    weak var value: T?
    init(_ value: T) { self.value = value }
}

extension HomeViewController {
    func loadData() {
        let box = WeakBox(self)
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            guard let self = box.value else { return }
            self.updateUI()
        }
    }
}

小结

  1. unowned 的唯一优点是 微不可计的性能提升;
  2. 当闭包寿命 可能 超过 self 寿命时,unowned = 定时炸弹;
  3. 在 UI 层、网络层、异步层,默认使用 weak;
  4. 若性能 profiling 证明 weak 成为热点,再考虑局部替换为 unowned,并加单元测试覆盖生命周期边界。

非逃逸闭包(non-escaping closure)—— 唯一 100% 安全的 unowned 场景

Swift 默认闭包是非逃逸的,编译器可以证明:闭包返回后,self 一定还活着。

// 数组的 forEach(非逃逸)
class Counter {
    var sum = 0
    func accumulate(_ nums: [Int]) {
        // 安全:forEach 同步执行,执行完 self 才退出作用域
        nums.forEach { [unowned self] n in
            self.sum += n
        }
    }
}

底层原理:

SIL(Swift Intermediate Language)在 optimize_lifetime 阶段会插入 fix_lifetime 指令,确保 self 在闭包调用期间不会被提前释放。

手动延长生命周期 —— withExtendedLifetime

// 同步网络请求回调,临时用 unowned 避免循环,但又要防崩溃
final class SyncAPIClient {
    func getUser() -> User? {
        var result: User?
        // 保证 self 在 block 返回前不会被释放
        withExtendedLifetime(self) {
            self.networkBlockAndWait { [unowned self] data in
                result = self.parse(data)   // 安全:self 被延长
            }
        }
        return result
    }
}

withExtendedLifetime 的源码实现:

@inlinable
public func withExtendedLifetime<T, Result>(
    _ x: T, 
    _ body: () throws -> Result
) rethrows -> Result {
    defer { _fixLifetime(x) }   // 防止编译器提前释放
    return try body()
}

原文资料

  1. blog.jacobstechtavern.com/p/the-case-…