注册
iOS

Swift 反初始化器详解——在实例永远“消失”之前,把该做的事做完

为什么要“反初始化”

  1. ARC 已经帮我们释放了内存,但“内存”≠“资源”。

    可能你打开过文件、有过数据库连接、订阅过通知、甚至握着 GPU 纹理句柄。

  2. 反初始化器(deinit)是 Swift 给你“最后一声道别”的钩子:

    实例即将被销毁 → 系统自动调用 → 你可以把文件关掉、把硬币还回银行、把日志写盘……

  3. 只有 class 有 deinit,struct / enum 没有;一个类最多一个 deinit;不允许手动显式调用。

deinit 的 6 条铁律

  1. 无参无括号:
class MyCls {
deinit { // 不能写 deinit() { ... }
// 清理代码
}
}
  1. 自动调用,调用顺序:子类 deinit 执行完 → 父类 deinit 自动执行。
  2. 实例“还没死”:deinit 里可访问任意 self 属性,甚至可调用实例方法。
  3. 不能自己调、不能重载、不能抛异常、不能带 async。
  4. 如果实例从未被真正强引用(例如刚 init 就赋 nil),deinit 不会触发。
  5. 若存在循环引用(strong reference cycle),deinit 永远不会触发——必须先解环。

示例

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()

运行结果

=== 游戏开始 ===
银行发放 100 枚,剩余 9900
玩家初始化,钱包得到 100
玩家当前硬币:100
银行发放 2000 枚,剩余 7900
玩家赢得 2000,钱包现在 2100
玩家离开,引用置 nil
玩家 deinit 开始,归还 2100
银行回收 2100 枚,当前 10000
玩家 deinit 结束
=== 游戏结束 ===

3 个高频扩展场景

  1. 关闭文件句柄
class Logger {
private let handle: FileHandle
init(path: String) throws {
handle = try FileHandle(forWritingTo: URL(fileURLWithPath: path))
}
deinit {
handle.closeFile() // 文件一定会被关掉
}
}
  1. 注销通知中心观察者
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)
}
}
  1. 释放手动分配的 C 内存 / GPU 纹理
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 当成“遗嘱执行人”

  1. 它只负责“身后事”:释放非内存资源、归还全局状态、写日志。
  2. 它不能保命:如果实例因为循环引用一直活着,就永远走不到 deinit。
  3. 它不能抢戏:别在 deinit 里做耗时任务(网络、IO),否则可能阻塞主线程或单元测试。
  4. 用好 weak / unowned + deinit,可以让 Swift 代码在“自动”与“可控”之间取得最佳平衡。

深入底层:deinit 在 SIL & 运行时到底做了什么

swiftc -emit-sil main.swift mainsil

  1. SIL(Swift Intermediate Language)视角

    编译器会为每个类生成一个 sil_vtable,里面存放了类中的所有方法,可以看到deinit中调用的是Player.__deallocating_deinit

    image.png

    Player.__deallocating_deinit中调用的 Player.__isolated_deallocating_deinit

    image.png

    Player.__isolated_deallocating_deinit中调用Player.deinit

    image.png

    伪代码:

   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,再跑成员销毁,最后归还堆内存。

  1. 运行时视角

    Swift 对象头部有一个 32-byte 的 HeapObject,其中 refCounts 字段采用“Side Table” 策略。

    当最后一次 swift_release 把引用计数降到 0 时,会立即跳到 destroy 函数指针 → 也就是上面的 SIL 函数。

    因此:

    • deinit 执行线程 = 最后一次 release 发生的线程;
    • deinit 执行耗时 ≈ 对象大小 + 成员销毁耗时 + 你写的代码耗时;
    • 如果 deinit 里再产生强引用(例如把 self 塞进全局数组),对象会被“复活”,但 Swift 5.5 之后禁止这种 resurrection,会直接 trap。

多线程与 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

  1. 继承链
@objc class BaseNS: NSObject {
deinit { print("Swift deinit") } // 实际上会生成 -dealloc 方法
}

编译器把 deinit 映射成 Objective-C 的 -dealloc,并在末尾自动插入 [super dealloc](ARC 下自动插入)。
2. 混编时序

  • Swift 侧先跑完 deinit;
  • 再跑 Objective-C 侧生成的 -dealloc
  • 最后 NSObject 的 -dealloc 释放 isa 与 ARC 附带内存。
  1. 注意点

    若你在 Objective-C 侧手动 override -dealloc,记得不要显式调用 [super dealloc](ARC 会自动加),否则编译报错。

Swift 5.9 新动向:move-only struct 的 deinit

SE-0390 已经落地 move-only ~Copyable struct,也可以写 deinit!

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 的性能损耗。

一张“思维导图”收尾

class 实例

├─ refCount == 0
├─ 否:继续浪
└─ 是:进入 destroy 流水线
1. 子类 deinit
2. 父类 deinit
3. 销毁所有存储属性
4. 归还堆内存

├─ 线程:最后一次 release 线程
├─ 复活:Swift 5.5+ 禁止,直接 trap

彩蛋:把 deinit 做成“叮”一声

#if DEBUG
deinit {
// 只调一次,不会循环引用
DispatchQueue.main.async {
AudioServicesPlaySystemSound(1057) // 键盘“叮”
}
}
#endif

每次对象销毁都会“叮”,办公室同事会投来异样眼光,但你能瞬间听出内存泄漏——当该响的没响,就说明循环引用啦!


作者:unravel2025
来源:juejin.cn/post/7566289235347816486

0 个评论

要回复文章请先登录注册