为什么要"反初始化"
-
ARC 已经帮我们释放了内存,但"内存"≠"资源"。
可能你打开过文件、有过数据库连接、订阅过通知、甚至握着 GPU 纹理句柄。
-
反初始化器(deinit)是 Swift 给你"最后一声道别"的钩子:
实例即将被销毁 → 系统自动调用 → 你可以把文件关掉、把硬币还回银行、把日志写盘......
-
只有 class 有 deinit,struct / enum 没有;一个类最多一个 deinit;不允许手动显式调用。
deinit 的 6 条铁律
- 无参无括号:
swift
class MyCls {
deinit { // 不能写 deinit() { ... }
// 清理代码
}
}
- 自动调用,调用顺序:子类 deinit 执行完 → 父类 deinit 自动执行。
- 实例"还没死":deinit 里可访问任意 self 属性,甚至可调用实例方法。
- 不能自己调、不能重载、不能抛异常、不能带 async。
- 如果实例从未被真正强引用(例如刚 init 就赋 nil),deinit 不会触发。
- 若存在循环引用(strong reference cycle),deinit 永远不会触发------必须先解环。
示例
swift
import Foundation
// MARK: - 银行:管理游戏世界唯一货币
@MainActor
class Bank {
// 静态共享实例 + 私有初始化,保证"全世界只有一家银行"
static let shared = Bank()
private init() {}
// 剩余硬币,private(set) 让外部只读
private(set) var coinsInBank = 10_000
/// 发放硬币;返回实际发出的数量(可能不够)
func distribute(coins number: Int) -> Int {
let numberToVend = min(number, coinsInBank)
coinsInBank -= numberToVend
print("银行发放 \(numberToVend) 枚,剩余 \(coinsInBank)")
return numberToVend
}
/// 回收硬币
func receive(coins number: Int) {
coinsInBank += number
print("银行回收 \(number) 枚,当前 \(coinsInBank)")
}
}
// MARK: - 玩家:从银行拿硬币,离开时自动归还
@MainActor
class Player {
var coinsInPurse: Int
/// 指定构造器:向银行申请"启动资金"
init(coins: Int) {
let received = Bank.shared.distribute(coins: coins)
coinsInPurse = received
print("玩家初始化,钱包得到 \(received)")
}
/// 赢钱:从银行再拿一笔
func win(coins: Int) {
let won = Bank.shared.distribute(coins: coins)
coinsInPurse += won
print("玩家赢得 \(won),钱包现在 \(coinsInPurse)")
}
/// 反初始化器:人走茶不凉,硬币先还银行
@MainActor
deinit {
print("玩家 deinit 开始,归还 \(coinsInPurse)")
Bank.shared.receive(coins: coinsInPurse)
print("玩家 deinit 结束")
}
}
// MARK: - 游戏主流程
@MainActor
func gameDemo() {
print("=== 游戏开始 ===")
// 1. 创建玩家;注意用可选类型,因为玩家随时可能 leave
var playerOne: Player? = Player(coins: 100)
// 如果不加调试打印,可简写:playerOne?.win(coins: 2000)
if let p = playerOne {
print("玩家当前硬币:\(p.coinsInPurse)")
p.win(coins: 2_000)
}
// 2. 玩家离开游戏;引用置 nil → 强引用归零 → deinit 被调用
print("玩家离开,引用置 nil")
playerOne = nil
print("=== 游戏结束 ===")
}
gameDemo()
运行结果
yaml
=== 游戏开始 ===
银行发放 100 枚,剩余 9900
玩家初始化,钱包得到 100
玩家当前硬币:100
银行发放 2000 枚,剩余 7900
玩家赢得 2000,钱包现在 2100
玩家离开,引用置 nil
玩家 deinit 开始,归还 2100
银行回收 2100 枚,当前 10000
玩家 deinit 结束
=== 游戏结束 ===
3 个高频扩展场景
- 关闭文件句柄
swift
class Logger {
private let handle: FileHandle
init(path: String) throws {
handle = try FileHandle(forWritingTo: URL(fileURLWithPath: path))
}
deinit {
handle.closeFile() // 文件一定会被关掉
}
}
- 注销通知中心观察者
swift
class KeyboardManager {
private var tokens: [NSObjectProtocol] = []
init() {
tokens.append(
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { _ in }
)
}
deinit {
tokens.forEach(NotificationCenter.default.removeObserver)
}
}
- 释放手动分配的 C 内存 / GPU 纹理
swift
class Texture {
private var raw: UnsafeMutableRawPointer?
init(size: Int) {
raw = malloc(size)
}
deinit {
free(raw) // 防止内存泄漏
}
}
常见踩坑与排查清单
| 现象 | 可能原因 | 排查工具 |
|---|---|---|
| deinit 从不打印 | 出现强引用循环 | Xcode Memory Graph / leaks 命令 |
| 子类 deinit 未调用 | 父类 init 失败提前 return | 在 init 各阶段加打印 |
| 访问属性崩溃 | 在 deinit 里访问了 weak / unowned 已释放属性 | 改用 strong 或提前判空 |
小结:把 deinit 当成"遗嘱执行人"
- 它只负责"身后事":释放非内存资源、归还全局状态、写日志。
- 它不能保命:如果实例因为循环引用一直活着,就永远走不到 deinit。
- 它不能抢戏:别在 deinit 里做耗时任务(网络、IO),否则可能阻塞主线程或单元测试。
- 用好 weak / unowned + deinit,可以让 Swift 代码在"自动"与"可控"之间取得最佳平衡。
深入底层:deinit 在 SIL & 运行时到底做了什么
swiftc -emit-sil main.swift mainsil
-
SIL(Swift Intermediate Language)视角
编译器会为每个类生成一个
sil_vtable,里面存放了类中的所有方法,可以看到deinit中调用的是Player.__deallocating_deinit
Player.__deallocating_deinit中调用的Player.__isolated_deallocating_deinit
Player.__isolated_deallocating_deinit中调用Player.deinit
伪代码:
less
sil @destroy_Player : $@convention(method) (@owned Player) -> () {
bb0(%0 : $Player):
// 1. 调用 deinit
%2 = function_ref @$s4main6PlayerCfZ : $@convention(thin) (@owned Player) -> ()
%3 = apply %2(%0) : $@convention(method) (@guaranteed Player) -> @owned Builtin.NativeObject // user: %4
// 2. 销毁存储属性
destroy_addr %0.#coinsInPurse
// 3. 释放整个对象内存
strong_release %5
}
结论:deinit 只是"销毁流水线"里的一环;先跑 deinit,再跑成员销毁,最后归还堆内存。
-
运行时视角
Swift 对象头部有一个 32-byte 的
HeapObject,其中refCounts字段采用"Side Table" 策略。当最后一次
swift_release把引用计数降到 0 时,会立即跳到destroy函数指针 → 也就是上面的 SIL 函数。因此:
- deinit 执行线程 = 最后一次
release发生的线程; - deinit 执行耗时 ≈ 对象大小 + 成员销毁耗时 + 你写的代码耗时;
- 如果 deinit 里再产生强引用(例如把 self 塞进全局数组),对象会被"复活",但 Swift 5.5 之后禁止这种 resurrection,会直接 trap。
- deinit 执行线程 = 最后一次
多线程与 deinit 的 4 个实战坑
| 场景 | 风险 | 正确姿势 |
|---|---|---|
| 子线程释放主线程创建的实例 | deinit 里刷新 UI | 用 DispatchQueue.main.async 或 MainActor.assertIsolated() |
| deinit 里加锁 | 可能和 init 锁顺序相反 → 死锁 | 尽量无锁;必须加锁时统一层级 |
deinit 里用 unowned 访问外部对象 |
外部对象可能已释放 | 改用 weak 并判空 |
| deinit 里继续派发异步任务 | 任务持有 self → 循环复活 | 使用 Task { [weak self] in ... } |
与 Objective-C 的交叉:dealloc vs deinit
- 继承链
swift
@objc class BaseNS: NSObject {
deinit { print("Swift deinit") } // 实际上会生成 -dealloc 方法
}
编译器把 deinit 映射成 Objective-C 的 -dealloc,并在末尾自动插入 [super dealloc](ARC 下自动插入)。
- 混编时序
- Swift 侧先跑完 deinit;
- 再跑 Objective-C 侧生成的
-dealloc; - 最后 NSObject 的
-dealloc释放 isa 与 ARC 附带内存。
-
注意点
若你在 Objective-C 侧手动 override
-dealloc,记得不要显式调用[super dealloc](ARC 会自动加),否则编译报错。
Swift 5.9 新动向:move-only struct 的 deinit
SE-0390 已经落地 move-only ~Copyable struct,也可以写 deinit!
swift
struct FileDescriptor: ~Copyable {
private let fd: Int32
init(path: String) throws { fd = open(path, O_RDONLY) }
deinit { // struct 也能有 deinit!
close(fd)
}
}
规则:
- 只要值被消耗(consume)或生命周期结束,deinit 就执行;
- 不能同时实现
deinit和Copyable; - 用于文件句柄、GPU 描述符等"必须唯一所有权"场景,彻底告别
class+deinit的性能损耗。
一张"思维导图"收尾
arduino
class 实例
│
├─ refCount == 0 ?
│ ├─ 否:继续浪
│ └─ 是:进入 destroy 流水线
│ 1. 子类 deinit 跑
│ 2. 父类 deinit 跑
│ 3. 销毁所有存储属性
│ 4. 归还堆内存
│
├─ 线程:最后一次 release 线程
├─ 复活:Swift 5.5+ 禁止,直接 trap
彩蛋:把 deinit 做成"叮"一声
swift
#if DEBUG
deinit {
// 只调一次,不会循环引用
DispatchQueue.main.async {
AudioServicesPlaySystemSound(1057) // 键盘"叮"
}
}
#endif
每次对象销毁都会"叮",办公室同事会投来异样眼光,但你能瞬间听出内存泄漏------当该响的没响,就说明循环引用啦!