1. 引言
在 Swift 中,析构过程(Deinitialization)是类实例在被释放前执行清理工作的机制。与 Objective-C 的dealloc方法类似,Swift 通过deinit关键字定义析构器,用于释放资源、保存状态或执行必要的收尾操作。本文将深入探讨 Swift 析构过程的工作原理,并通过丰富的代码实例帮助读者全面掌握这一重要特性。
2. 析构过程的基本原理
Swift 使用自动引用计数(ARC)来管理类实例的内存。当一个类的实例不再被强引用时,ARC 会自动释放该实例的内存,并在释放前调用其析构器deinit。
析构器具有以下特点:
- 每个类最多只能有一个析构器。
- 析构器不接受任何参数,也不返回任何值。
- 析构器由系统在实例释放前自动调用,不能手动调用。
- 析构器使用
deinit关键字声明,不需要写func关键字。
3. 基础析构器示例
下面是一个最简单的析构器示例,演示了deinit的基本语法和调用时机:
class BankAccount { var balance: Double init(balance: Double) { self.balance = balance print("账户创建成功,初始余额:\(balance) 元") } deinit { print("账户已销毁,释放相关资源") } } // 使用示例 var account: BankAccount? = BankAccount(balance: 1000.0) account = nil // 输出: // 账户创建成功,初始余额:1000.0 元 // 账户已销毁,释放相关资源当account被设置为nil时,该实例不再被强引用,ARC 立即释放其内存,并自动调用deinit方法。
4. 析构器中的资源清理
析构器最常见的用途是释放外部资源,例如关闭文件句柄、断开网络连接或释放数据库连接。下面通过一个文件管理器的示例来说明:
import Foundation class FileManagerHandler { let filePath: String var fileHandle: FileHandle? init(filePath: String) { self.filePath = filePath // 模拟打开文件 fileHandle = FileHandle(forReadingAtPath: filePath) print("文件已打开:\(filePath)") } func readData() -> Data? { return fileHandle?.readDataToEndOfFile() } deinit { // 关闭文件句柄,释放资源 fileHandle?.closeFile() print("文件已关闭,资源已释放:\(filePath)") } } // 使用示例 var handler: FileManagerHandler? = FileManagerHandler(filePath: "/tmp/example.txt") let data = handler?.readData() handler = nil // 输出: // 文件已打开:/tmp/example.txt // 文件已关闭,资源已释放:/tmp/example.txt5. 析构器与类继承
在 Swift 中,子类会继承父类的析构器。当子类实例被释放时,会先执行子类的析构器,再执行父类的析构器。这与初始化过程(先执行父类初始化,再执行子类初始化)的顺序正好相反。
class Animal { var name: String init(name: String) { self.name = name print("\(name) 初始化完成") } deinit { print("\(name) 的父类析构器执行") } } class Dog: Animal { var breed: String init(name: String, breed: String) { self.breed = breed super.init(name: name) print("\(name) 子类初始化完成") } deinit { print("\(name) 的子类析构器执行") } } // 使用示例 var dog: Dog? = Dog(name: "旺财", breed: "金毛") dog = nil // 输出: // 旺财 初始化完成 // 旺财 子类初始化完成 // 旺财 的子类析构器执行 // 旺财 的父类析构器执行6. 析构器与可选类型
只有类类型的可选变量才能触发析构器。当可选变量被设置为nil时,如果该变量是该实例的最后一个强引用,ARC 就会释放实例并调用析构器。下面通过一个日志记录器的示例来说明:
class Logger { let identifier: String init(identifier: String) { self.identifier = identifier print("日志记录器 \(identifier) 已创建") } func log(_ message: String) { print("[\(identifier)] \(message)") } deinit { print("日志记录器 \(identifier) 已销毁") } } // 使用示例 var logger: Logger? = Logger(identifier: "main") logger?.log("应用启动") logger = nil logger?.log("这条日志不会输出") // 输出: // 日志记录器 main 已创建 // [main] 应用启动 // 日志记录器 main 已销毁7. 析构器与闭包捕获
当闭包捕获了类实例的self时,如果闭包被该实例持有,就会形成循环引用,导致析构器永远不会被调用。下面通过一个反面示例来说明这个问题:
class NetworkManager { var onComplete: (() -> Void)? init() { print("网络管理器已创建") } func startRequest() { // 闭包捕获 self,形成循环引用 onComplete = { print("请求完成,回调执行") self.handleResponse() } } func handleResponse() { print("处理响应数据") } deinit { print("网络管理器已销毁") } } // 使用示例 var manager: NetworkManager? = NetworkManager() manager?.startRequest() manager = nil // 输出: // 网络管理器已创建 // 注意:析构器没有被调用,因为存在循环引用为了解决循环引用问题,可以使用[weak self]或[unowned self]捕获列表:
class NetworkManagerFixed { var onComplete: (() -> Void)? init() { print("网络管理器已创建") } func startRequest() { // 使用 weak self 打破循环引用 onComplete = { [weak self] in guard let self = self else { return } print("请求完成,回调执行") self.handleResponse() } } func handleResponse() { print("处理响应数据") } deinit { print("网络管理器已销毁") } } // 使用示例 var fixedManager: NetworkManagerFixed? = NetworkManagerFixed() fixedManager?.startRequest() fixedManager = nil // 输出: // 网络管理器已创建 // 网络管理器已销毁8. 析构器与存储属性
在析构器中,可以访问实例的所有存储属性,包括可选属性和常量属性。但需要注意的是,析构器执行完毕后,实例的内存才会被真正释放,因此不能在析构器中访问已经被释放的属性。
class DatabaseConnection { let connectionID: String var isConnected: Bool var lastQuery: String? init(connectionID: String) { self.connectionID = connectionID self.isConnected = true print("数据库连接 \(connectionID) 已建立") } func executeQuery(_ query: String) { lastQuery = query print("执行查询:\(query)") } deinit { // 在析构器中访问存储属性 print("正在关闭数据库连接:\(connectionID)") print("最后执行的查询:\(lastQuery ?? "无")") isConnected = false print("连接状态已更新:\(isConnected)") } } // 使用示例 var connection: DatabaseConnection? = DatabaseConnection(connectionID: "conn-001") connection?.executeQuery("SELECT * FROM users") connection = nil // 输出: // 数据库连接 conn-001 已建立 // 执行查询:SELECT * FROM users // 正在关闭数据库连接:conn-001 // 最后执行的查询:SELECT * FROM users // 连接状态已更新:false9. 析构器与单例模式
单例模式中,实例通常被全局持有,因此析构器在应用生命周期内不会被调用。下面通过一个配置管理器的示例来说明:
class AppConfiguration { static let shared = AppConfiguration() var theme: String = "light" var language: String = "zh-CN" private init() { print("配置管理器初始化") } func printConfiguration() { print("主题:\(theme),语言:\(language)") } deinit { // 单例的析构器通常不会被调用 print("配置管理器析构(正常情况下不会执行)") } } // 使用示例 let config = AppConfiguration.shared config.printConfiguration() // 输出: // 配置管理器初始化 // 主题:light,语言:zh-CN10. 析构器与结构体
需要特别注意的是,结构体(struct)和枚举(enum)是值类型,不支持析构器。只有类(class)这种引用类型才支持deinit。下面通过一个对比示例来说明:
// 结构体不支持析构器 struct Point { var x: Double var y: Double // 编译错误:结构体不能包含析构器 // deinit { // print("Point 析构") // } } // 类支持析构器 class Circle { var center: Point var radius: Double init(center: Point, radius: Double) { self.center = center self.radius = radius print("圆形创建成功") } func area() -> Double { return Double.pi * radius * radius } deinit { print("圆形已销毁") } } // 使用示例 var circle: Circle? = Circle(center: Point(x: 0, y: 0), radius: 5.0) print("面积:\(circle?.area() ?? 0)") circle = nil // 输出: // 圆形创建成功 // 面积:78.53981633974483 // 圆形已销毁11. 析构器与自动释放池
在 Swift 中,ARC 会在最后一次强引用被移除时立即释放实例,不需要像 Objective-C 那样依赖自动释放池。但在某些场景下,理解释放时机仍然很重要。下面通过一个批量处理示例来说明:
class Task { let taskID: Int init(taskID: Int) { self.taskID = taskID print("任务 \(taskID) 创建") } func execute() { print("任务 \(taskID) 执行中") } deinit { print("任务 \(taskID) 销毁") } } // 使用示例 func processTasks() { for i in 1...3 { let task = Task(taskID: i) task.execute() // 每次循环结束后,task 离开作用域,立即释放 } } processTasks() // 输出: // 任务 1 创建 // 任务 1 执行中 // 任务 1 销毁 // 任务 2 创建 // 任务 2 执行中 // 任务 2 销毁 // 任务 3 创建 // 任务 3 执行中 // 任务 3 销毁12. 析构器与通知中心
在 iOS 开发中,使用通知中心(NotificationCenter)时,需要在析构器中移除观察者,避免内存泄漏和野指针问题。下面通过一个示例来说明:
import Foundation class NotificationObserver { let observerName: String init(observerName: String) { self.observerName = observerName // 添加通知观察者 NotificationCenter.default.addObserver( self, selector: #selector(handleNotification(_:)), name: NSNotification.Name("CustomNotification"), object: nil ) print("\(observerName) 已注册通知观察者") } @objc func handleNotification(_ notification: Notification) { print("\(observerName) 收到通知:\(notification.userInfo ?? [:])") } deinit { // 移除通知观察者,避免野指针 NotificationCenter.default.removeObserver(self) print("\(observerName) 已移除通知观察者") } } // 使用示例 var observer: NotificationObserver? = NotificationObserver(observerName: "主观察者") NotificationCenter.default.post( name: NSNotification.Name("CustomNotification"), object: nil, userInfo: ["message": "Hello"] ) observer = nil // 输出: // 主观察者 已注册通知观察者 // 主观察者 收到通知:["message": "Hello"] // 主观察者 已移除通知观察者13. 析构器与定时器
使用定时器(Timer)时,如果定时器持有对目标的强引用,也会导致循环引用。正确的做法是在析构器中使定时器失效。下面通过一个示例来说明:
import Foundation class TimerManager { var timer: Tim