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

为什么要"反初始化"

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

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

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

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

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

deinit 的 6 条铁律

  1. 无参无括号:
swift 复制代码
class MyCls {
    deinit { // 不能写 deinit() { ... }
        // 清理代码
    }
}
  1. 自动调用,调用顺序:子类 deinit 执行完 → 父类 deinit 自动执行。
  2. 实例"还没死":deinit 里可访问任意 self 属性,甚至可调用实例方法。
  3. 不能自己调、不能重载、不能抛异常、不能带 async。
  4. 如果实例从未被真正强引用(例如刚 init 就赋 nil),deinit 不会触发。
  5. 若存在循环引用(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 个高频扩展场景

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

  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

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

  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.asyncMainActor.assertIsolated()
deinit 里加锁 可能和 init 锁顺序相反 → 死锁 尽量无锁;必须加锁时统一层级
deinit 里用 unowned 访问外部对象 外部对象可能已释放 改用 weak 并判空
deinit 里继续派发异步任务 任务持有 self → 循环复活 使用 Task { [weak self] in ... }

与 Objective-C 的交叉:dealloc vs deinit

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

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

  1. 混编时序
  • 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!

swift 复制代码
struct FileDescriptor: ~Copyable {
    private let fd: Int32
    init(path: String) throws { fd = open(path, O_RDONLY) }
    deinit {               // struct 也能有 deinit!
        close(fd)
    }
}

规则:

  • 只要值被消耗(consume)或生命周期结束,deinit 就执行;
  • 不能同时实现 deinitCopyable
  • 用于文件句柄、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

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

相关推荐
HarderCoder8 小时前
Swift 并发编程新选择:Mutex 保护可变状态实战解析
swift
HarderCoder1 天前
Swift 模式:解构与匹配的安全之道
swift
东坡肘子1 天前
Swift 官方发布 Android SDK | 肘子的 Swift 周报 #0108
android·swiftui·swift
YGGP3 天前
【Swift】LeetCode 53. 最大子数组和
swift
2501_916008893 天前
用多工具组合把 iOS 混淆做成可复用的工程能力(iOS混淆|IPA加固|无源码混淆|Ipa Guard|Swift Shield)
android·开发语言·ios·小程序·uni-app·iphone·swift
胎粉仔3 天前
Swift 初阶 —— inout 参数 & 数据独占问题
开发语言·ios·swift·1024程序员节
HarderCoder3 天前
Swift 下标(Subscripts)详解:从基础到进阶的完整指南
swift
YGGP3 天前
【Swift】LeetCode 189. 轮转数组
swift
JZXStudio3 天前
5.A.swift 使用指南
框架·swift·app开发