现在位置: 首页 > Swift 教程 > 正文

Swift 析构过程

析构器(deinitializer)在类的实例被释放之前自动调用,用来完成资源清理工作。

用关键字 deinit 声明,只有类类型才有析构器,结构体和枚举没有。


deinit 语法与执行时机

每个类最多只能有一个析构器,它不带任何参数,写法上也不写括号。

deinit {
    // 执行清理工作
}

析构器由系统在实例即将被释放时自动调用,你不需要、也不能自己发起调用。

下面的例子用一个静态计数器观察实例的创建与释放。

实例

import Foundation

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 后,它原来持有的实例失去引用,析构器随即被调用。

更常见的触发时机是作用域结束:局部变量在函数返回时销毁,析构器自然执行。

实例

import Foundation

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)管理实例的内存,析构器的执行时机正是由引用计数决定的。

只要还有一个强引用指向实例,实例就不会被释放;当最后一个强引用消失,引用计数归零,析构器才会执行。

实例

import Foundation

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
程序结束

如果两个对象互相持有对方的强引用,就会形成引用循环,引用计数永远不会归零,析构器也就一直不会执行。

实例

import Foundation

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 后没有任何析构信息,说明两个对象都没有被释放

解决办法是把其中一个引用改成 weakunowned,相关内容可以参阅《Swift 自动引用计数》。


常见问题

析构器本身语法简单,问题往往出在对释放时机的理解上。

deinit 一直没有被调用

最常见的原因是存在循环强引用,或者某个全局变量、闭包长期持有该实例。检查一下对象关系图中是否存在互相强引用。

能否在 deinit 里访问其他属性

可以读取实例自己的属性,但此时实例即将销毁,不应再把它自己或属性传递出去交给别人长期持有,否则会引发难以排查的问题。

结构体为什么没有 deinit

析构器只用于类。值类型由编译器在作用域结束时直接销毁,没有自定义清理逻辑的入口,需要清理资源时应封装成类。