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

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

相关推荐
用户794572239541313 小时前
【AFNetworking】OC 时代网络请求事实标准,Alamofire 的前身
objective-c·swift
报错小能手16 小时前
SwiftUI 框架 认识 SwiftUI 视图结构 + 布局
ui·ios·swift
东坡肘子17 小时前
被 Vibe 摧毁的版权壁垒,与开发者的新护城河 -- 肘子的 Swift 周报 #131
人工智能·swiftui·swift
报错小能手1 天前
ios开发方向——swift错误处理:do/try/catch、Result、throws
开发语言·学习·ios·swift
小夏子_riotous1 天前
openstack的使用——5. Swift服务的基本使用
linux·运维·开发语言·分布式·云计算·openstack·swift
mCell2 天前
MacOS 下实现 AI 操控电脑(Computer Use)的思考
macos·agent·swift
用户79457223954132 天前
【DGCharts】iOS 图表渲染事实标准——8 种图表类型、高度可定制,3 行代码画出一条折线
swiftui·swift
chaoguo12342 天前
Any metadata 的内存布局
swift·metadata·value witness table
tangweiguo030519873 天前
SwiftUI布局完全指南:从入门到精通
ios·swift
用户79457223954134 天前
【RxSwift】Swift 版 ReactiveX,响应式编程优雅处理异步事件流
swift·rxswift