在 Swift 的 ARC(自动引用计数)机制下,内存泄漏(Memory Leak)的本质是:对象的引用计数永远无法归零。
即便你已经不再使用这些对象,它们依然占据着堆内存,导致程序的内存占用持续上升,严重时会引发 OOM(内存溢出)崩溃。
以下是导致 ARC 无法释放对象的几种核心情况:
1. 循环强引用 (Strong Reference Cycles)
这是最常见、最经典的内存泄漏场景。当两个或多个对象互相持有对方的强引用时,它们的引用计数永远至少为 1,形成了一个逻辑上的“死结”。
场景 A:对象间的互相持有
Swift
class Teacher {
var student: Student?
}
class Student {
var teacher: Teacher?
}
var t: Teacher? = Teacher()
var s: Student? = Student()
t?.student = s
s?.teacher = t
t = nil // t 变量断开了,但对象 Teacher 里的 student 还在引用对象 Student
s = nil // s 变量断开了,但对象 Student 里的 teacher 还在引用对象 Teacher
// 结果:两个对象在内存中“孤岛化”,互相拉扯,永不销毁。
场景 B:闭包捕获 (Closure Captures)
闭包(Closure)在 Swift 中也是引用类型。如果一个对象持有一个闭包,而这个闭包内部又使用了 self(强引用了该对象),就会形成循环。
Swift
class Controller {
var onComplete: (() -> Void)?
func setup() {
onComplete = {
// 闭包强引用了 self (Controller)
// Controller 又强引用了 onComplete (闭包)
print(self)
}
}
}
2. 线程与异步回调的延迟释放
虽然这不是严格意义上的“永久泄漏”,但会导致对象生命周期异常延长,看起来像泄漏。
- DispatchQueue 的存活:如果你在一个长生命周期的后台队列中强引用了
self,只要任务没执行完,对象就不会释放。 - Timer (定时器) :
Timer.scheduledTimer会强引用它的target。如果定时器不手动invalidate,被引用的对象(通常是 ViewController)将永远留在内存里。
3. 被全局或静态变量持有
由于静态变量(static)和全局变量的生命周期与整个应用程序相同,如果你不小心将一个大对象赋值给了静态属性且没有手动清空,它将永远无法释放。
Swift
class MemoryLeaker {
static var cache: [Any] = []
}
// 如果不断往 cache 里塞对象而不清理,这些对象就永远不会被 ARC 回收。
4. 观察者模式与通知中心 (NotificationCenter)
在旧版 Swift/iOS 开发中,如果你向 NotificationCenter 注册了观察者,但忘记在 deinit 中执行 removeObserver,某些情况下会导致闭包或对象被通知中心强引用,从而无法释放。 注:在 iOS 9 之后,某些系统通知已改为弱引用,但闭包形式的通知(addObserver(forName:...))依然需要手动处理销毁。
为什么会产生内存泄漏?(深层逻辑)
- 逻辑断层:程序员认为变量已经设为
nil了,但忽略了隐藏的引用路径(如闭包内部、集合容器内部)。 - ARC 的局限性:ARC 只能处理“引用计数”的增减,它不具备识别“孤岛循环”的能力。这与 Java/Go 的垃圾回收器(GC)不同,GC 会从根节点(Root)扫描,如果发现一组对象虽然互相引用但无法从根节点到达,GC 会清理它们;而 ARC 只要计数不为 0,就认为对象有效。
如何避免?
-
使用
weak(弱引用) :打破循环引用的首选。它不增加计数,且对象销毁后自动变nil。 -
使用
unowned(无主引用) :当你确定引用的对象生命周期比当前对象长(或一样长)时使用,性能比weak略高。 -
闭包捕获列表:在闭包中使用
[weak self]。 -
Xcode 工具:
- Memory Graph Debugger:可视化查看内存中的对象引用树,能直观看到循环引用的“圈”。
- Instruments (Leaks) :自动化检测运行时的内存泄漏。
英文版
8-6. [Memory Management] When does ARC fail to deallocate objects? Why do leaks occur?
In Swift's ARC mechanism, the essence of a Memory Leak is that an object's reference count can never reach zero.
Even if you are no longer using these objects, they continue to occupy heap memory. This causes the app's memory footprint to rise steadily, eventually leading to OOM (Out Of Memory) crashes.
1. Strong Reference Cycles (Retain Cycles)
This is the most common and classic cause of memory leaks. It occurs when two or more objects hold strong references to each other, ensuring their reference counts always remain at least 1, creating a logical "deadlock."
Scenario A: Mutual Holding between Objects
Swift
class Teacher {
var student: Student?
}
class Student {
var teacher: Teacher?
}
var t: Teacher? = Teacher()
var s: Student? = Student()
t?.student = s
s?.teacher = t
t = nil // Variable 't' is broken, but the Teacher object is still held by the Student object.
s = nil // Variable 's' is broken, but the Student object is still held by the Teacher object.
// Result: Both objects become an "island" in memory, pulling on each other and never deallocating.
Scenario B: Closure Captures
Closures in Swift are reference types. If an object holds a closure, and that closure internally uses self (strongly capturing the object), a cycle is formed.
Swift
class Controller {
var onComplete: (() -> Void)?
func setup() {
onComplete = {
// The closure strongly captures self (Controller)
// Controller strongly holds onComplete (the closure)
print(self)
}
}
}
2. Delayed Deallocation in Async Callbacks
While not always a "permanent" leak, this causes an object's lifecycle to extend abnormally, which mimics a leak.
- DispatchQueue Persistence: If you strongly capture
selfinside a long-running background task, the object will not be released until the task completes. - Timer Persistence:
Timer.scheduledTimerstrongly captures itstarget. If the timer is not manuallyinvalidate-ed, the referenced object (usually a ViewController) will stay in memory forever.
3. Holding by Global or Static Variables
Since the lifecycle of static and global variables matches the entire duration of the app, if you accidentally assign a large object to a static property and never clear it, it will never be deallocated.
Swift
class MemoryLeaker {
static var cache: [Any] = []
}
// If you keep stuffing objects into 'cache' without cleaning it,
// those objects will never be reclaimed by ARC.
4. NotificationCenter Observers
In older Swift/iOS development, if you registered an observer with NotificationCenter but forgot to call removeObserver in deinit, the observer closure or object could be strongly held by the center. Note: Since iOS 9, many system notifications use weak references, but closure-based observers (addObserver(forName:...)) still require manual cleanup.
Why do leaks occur? (The Deep Logic)
- Logical Gaps: Developers assume setting a variable to
nilis enough, overlooking hidden reference paths (inside closures, collections, or system frameworks). - ARC's Limitation: ARC only handles incrementing and decrementing counts; it cannot recognize "islands of isolation." This differs from Garbage Collectors (GC) in Java or Go. A GC scans from "Roots"; if it finds a group of objects that reference each other but cannot be reached from a Root, it clears them. ARC, however, assumes an object is valid as long as its count is not zero.
How to Prevent Leaks?
-
Use
weak(Weak Reference) : The primary tool to break cycles. It doesn't increase the count, and the reference automatically becomesnilwhen the object is destroyed. -
Use
unowned(Unowned Reference) : Used when you are certain the referenced object will outlive (or live as long as) the current object. It has slightly better performance thanweak. -
Closure Capture Lists: Always use
[weak self]in closures that might outlive the object. -
Xcode Debugging Tools:
- Memory Graph Debugger: Visually inspect the object reference tree to find "loops."
- Instruments (Leaks) : Automatically detect leaks during runtime.