Swift 析构过程
析构器(deinitializer)在类的实例被释放之前自动调用,用来完成资源清理工作。
用关键字 deinit 声明,只有类类型才有析构器,结构体和枚举没有。
deinit 语法与执行时机
每个类最多只能有一个析构器,它不带任何参数,写法上也不写括号。
deinit {
// 执行清理工作
}
析构器由系统在实例即将被释放时自动调用,你不需要、也不能自己发起调用。
下面的例子用一个静态计数器观察实例的创建与释放。
实例
class RunoobCounter {
static var count = 0
init() {
RunoobCounter.count += 1
print("创建实例,当前实例数:\(RunoobCounter.count)")
}
deinit {
RunoobCounter.count -= 1
print("释放实例,当前实例数:\(RunoobCounter.count)")
}
}
var a: RunoobCounter? = RunoobCounter()
var b: RunoobCounter? = RunoobCounter()
a = nil
b = nil
运行结果:
创建实例,当前实例数:1 创建实例,当前实例数:2 释放实例,当前实例数:1 释放实例,当前实例数:0
把可选变量赋值为 nil 后,它原来持有的实例失去引用,析构器随即被调用。
更常见的触发时机是作用域结束:局部变量在函数返回时销毁,析构器自然执行。
实例
class FileHandler {
let path: String
init(path: String) {
self.path = path
print("打开文件:\(path)")
}
deinit {
// 实例被释放前,关闭它持有的资源
print("关闭文件:\(path)")
}
}
func useFile() {
let handler = FileHandler(path: "www.runoob.com/data.txt")
print("正在使用 \(handler.path)")
// 函数结束时 handler 超出作用域,引用计数归零,deinit 被调用
}
useFile()
print("useFile 已返回")
运行结果:
打开文件:www.runoob.com/data.txt 正在使用 www.runoob.com/data.txt 关闭文件:www.runoob.com/data.txt useFile 已返回
注意输出的顺序:关闭文件出现在 useFile 已返回之前,说明析构发生在函数返回的那一刻。
不能手动调用、不能抛错
析构器由运行时自动调用,代码里直接写 instance.deinit 或类似调用是不允许的。
析构器也不能声明为 throws,不能抛出错误,因为它是在实例销毁流程中执行的,没有合适的调用方来接收错误。
| 限制 | 说明 |
|---|---|
| 数量 | 每个类最多一个析构器 |
| 参数 | 不能带任何参数,也不写括号 |
| 调用方式 | 只能由系统自动调用,不能手动调用 |
| 错误处理 | 不能抛错,也不能声明 throws |
| 适用类型 | 只有类有析构器 |
注意:子类析构器会在自己的清理逻辑执行完后自动调用父类析构器,不需要也不能显式写
super.deinit()。
与 ARC 的配合
Swift 用自动引用计数(ARC)管理实例的内存,析构器的执行时机正是由引用计数决定的。
只要还有一个强引用指向实例,实例就不会被释放;当最后一个强引用消失,引用计数归零,析构器才会执行。
实例
class Resource {
let name: String
init(name: String) {
self.name = name
print("初始化 \(name)")
}
deinit {
print("析构 \(name)")
}
}
var strongRef: Resource? = Resource(name: "Runoob")
var anotherRef = strongRef // 第二个强引用,引用计数变成 2
strongRef = nil // 引用计数减一,但对象仍然存活
print("strongRef 置为 nil 后,anotherRef 仍然可用:\(anotherRef!.name)")
anotherRef = nil // 最后一个强引用消失,deinit 才执行
print("程序结束")
运行结果:
初始化 Runoob strongRef 置为 nil 后,anotherRef 仍然可用:Runoob 析构 Runoob 程序结束
如果两个对象互相持有对方的强引用,就会形成引用循环,引用计数永远不会归零,析构器也就一直不会执行。
实例
class Node {
let name: String
var next: Node?
init(name: String) {
self.name = name
}
deinit {
print("析构 \(name)")
}
}
var first: Node? = Node(name: "A")
var second: Node? = Node(name: "B")
first?.next = second
second?.next = first // 互相强引用,形成引用循环
first = nil
second = nil
print("置为 nil 后没有任何析构信息,说明两个对象都没有被释放")
运行结果:
置为 nil 后没有任何析构信息,说明两个对象都没有被释放
解决办法是把其中一个引用改成 weak 或 unowned,相关内容可以参阅《Swift 自动引用计数》。
常见问题
析构器本身语法简单,问题往往出在对释放时机的理解上。
deinit 一直没有被调用
最常见的原因是存在循环强引用,或者某个全局变量、闭包长期持有该实例。检查一下对象关系图中是否存在互相强引用。
能否在 deinit 里访问其他属性
可以读取实例自己的属性,但此时实例即将销毁,不应再把它自己或属性传递出去交给别人长期持有,否则会引发难以排查的问题。
结构体为什么没有 deinit
析构器只用于类。值类型由编译器在作用域结束时直接销毁,没有自定义清理逻辑的入口,需要清理资源时应封装成类。
