Swift 自动引用计数(ARC)(长文讲解)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目:《Spring AI 项目实战》 正在持续爆肝中,基于 Spring AI + Spring Boot 3.x + JDK 21..., 点击查看 ;
- 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观
在 Swift 开发中,内存管理是一个既基础又复杂的主题。对于初学者而言,手动管理内存容易引发崩溃或性能问题,而中级开发者则可能因疏忽导致内存泄漏。Swift 自动引用计数(ARC) 正是为了解决这些问题而设计的机制,它通过自动跟踪对象的引用关系,确保内存高效且安全地释放。本文将从零开始,逐步解析 ARC 的原理、常见问题及解决方案,并通过案例帮助读者深入理解这一核心概念。
什么是自动引用计数(ARC)?
自动引用计数(Automatic Reference Counting, ARC) 是 Swift 的内存管理机制,其核心是通过跟踪对象被引用的次数来判断是否需要释放内存。简单来说,当某个对象没有被任何引用指向时,ARC 会自动回收该对象占用的内存空间。
引用计数的比喻
想象你和朋友在公园里玩捉迷藏。每个朋友被找到后,游戏组织者会记录他们的“参与次数”。当参与次数归零时,他们就可以离开公园回家。在 Swift 中,对象就像这些朋友,而“参与次数”就是引用计数。只要至少有一个引用指向对象,它的计数就不会归零,对象就会一直“留在公园里”。
ARC 的工作流程
- 创建对象时:ARC 会为该对象分配内存空间,并初始化引用计数为 1(因为至少有一个变量或常量指向它)。
- 增加引用时:每当一个新变量或常量指向该对象,引用计数加 1。
- 减少引用时:当变量或常量超出作用域或被重新赋值时,引用计数减 1。
- 释放内存:当引用计数降为 0 时,ARC 会立即释放该对象的内存。
强引用与循环引用问题
强引用的定义
在 Swift 中,默认情况下,变量或常量对对象的引用是“强引用”(Strong Reference)。强引用会增加对象的引用计数,且只有当所有强引用被移除后,对象才会被释放。
示例代码:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) 被创建")
}
deinit {
print("\(name) 被销毁")
}
}
var alice: Person?
alice = Person(name: "Alice") // Alice 的引用计数为 1
var bob = alice // Alice 的引用计数变为 2
alice = nil // Alice 的引用计数变为 1(因为 bob 仍指向它)
bob = nil // Alice 的引用计数归零,触发销毁
循环引用的产生
循环引用(Retain Cycle)是指两个或多个对象之间相互持有强引用,导致彼此的引用计数永远无法降为 0,最终内存泄漏。
示例:
class Dog {
let name: String
unowned let owner: Person // 这里需要特殊处理,避免循环
init(name: String, owner: Person) {
self.name = name
self.owner = owner
print("\(name) 被创建")
}
deinit { print("\(name) 被销毁") }
}
class Person {
let name: String
var dog: Dog?
init(name: String) {
self.name = name
print("\(name) 被创建")
}
deinit { print("\(name) 被销毁") }
}
var john: Person? = Person(name: "John")
john?.dog = Dog(name: "Buddy", owner: john!) // 这里存在循环引用
john = nil // 试图释放 John,但 Dog 的 owner 属性持有强引用,导致两者无法销毁
循环引用的成因分析
在上述代码中,Dog
的 owner
属性和 Person
的 dog
属性形成了一个闭环:
Dog
对Person
有强引用(因为owner
是强引用)。Person
通过dog
属性对Dog
也有强引用。
因此,当john
被设为nil
后,两者引用计数仍为 1,导致内存无法释放。
如何解决循环引用?
1. 使用 weak
关键字
weak
引用不会增加对象的引用计数,并且当对象被释放后,weak
引用会自动设为 nil
。它适用于“非拥有关系”,例如父类与子类、观察者与被观察者等场景。
修改后的代码:
class Dog {
let name: String
unowned let owner: Person // 改为 unowned 或 weak
init(name: String, owner: Person) {
self.name = name
self.owner = owner
}
deinit { print("\(name) 被销毁") }
}
// 使用 weak
class Person {
let name: String
weak var dog: Dog? // 将 dog 改为 weak
init(name: String) { self.name = name }
deinit { print("\(name) 被销毁") }
}
var john: Person? = Person(name: "John")
john?.dog = Dog(name: "Buddy", owner: john!)
john = nil // 此时 John 和 Buddy 都会被释放
2. 使用 unowned
关键字
unowned
与 weak
类似,但不会自动设为 nil
,且假设引用始终有效。它适用于父对象生命周期长于子对象的场景(例如视图和控制器的关系)。
示例:
class ViewController {
unowned let label: UILabel
init(label: UILabel) {
self.label = label // 假设 ViewController 的生命周期不短于 label
}
}
3. 解决闭包中的循环引用
闭包默认会捕获外部变量的强引用,若闭包本身被强引用持有,会形成循环。此时可用 [weak self]
或 [unowned self]
来打破循环。
示例:
class TimerManager {
var completion: (() -> Void)?
init() {
completion = {
[weak self] in
print("Timer 完成")
self?.completion = nil // 手动断开引用
}
}
deinit { print("TimerManager 被销毁") }
}
var tm: TimerManager? = TimerManager()
tm = nil // 此时 TimerManager 可以被释放
实战案例:构建一个安全的观察者模式
观察者模式常用于事件监听,但若处理不当,容易引发循环引用。
问题代码:
class Observable<T> {
private var listener: ((T) -> Void)?
func addListener(_ closure: @escaping (T) -> Void) {
listener = closure // 此处形成循环引用
}
func notify(value: T) {
listener?(value)
}
}
class Observer {
var observable: Observable<Int>?
init(observable: Observable<Int>) {
self.observable = observable
observable.addListener { [weak self] value in
print("收到值: \(value)")
}
}
deinit { print("Observer 被销毁") }
}
// 测试
var obs: Observer? = Observer(observable: Observable<Int>())
obs = nil // 此时 Observable 和 Observer 都会被释放
分析与解决方案
在原始代码中,Observable
的 listener
闭包捕获了 Observer
的强引用,而 Observer
又持有 Observable
的强引用,形成循环。通过在闭包中使用 [weak self]
,成功打破循环,确保对象能够被释放。
总结
Swift 自动引用计数(ARC) 是开发者必须掌握的核心概念。通过理解强引用、弱引用(weak)、无主引用(unowned)以及闭包捕获规则,可以有效避免内存泄漏和崩溃问题。
- 关键点回顾:
- 引用计数是 ARC 的核心机制,对象的生命周期由其引用关系决定。
- 循环引用常见于对象间相互强引用的场景,需通过
weak
或unowned
解决。 - 闭包中的循环引用需通过捕获列表(Capture List)显式管理。
掌握这些技巧后,开发者可以更自信地构建复杂的应用逻辑,同时确保程序的高效与稳定性。记住:在 Swift 中,“引用关系决定生死”,合理管理每一处引用,是写出健壮代码的关键。