iOS内存管理
关于强引用循环
强引用循环是 ARC 无法自动处理的常见问题。如果两个对象互相强引用对方,就会造成引用计数不为零,导致对象无法释放。典型的情况是在闭包中引用 self
时,self
和闭包之间可能会互相持有,形成强引用循环。
解决方法: 使用 [weak self]
或 [unowned self]
来避免强引用循环,特别是在闭包内。
强引用(Strong Reference)是指一个对象在内存中持有对另一个对象的引用时,这个引用会增加被引用对象的引用计数。当一个对象的引用计数增加时,它的生命周期就被延长,直到引用计数变为零,表示没有任何对象引用它时,ARC 会自动释放这个对象的内存。
在 iOS 中,默认情况下,所有对象的引用都是强引用。强引用会导致引用的对象在内存中不会被释放,直到所有强引用都解除。
强引用的基本理解
- 强引用:指对象通过
strong
持有另一个对象。该对象的生命周期会被引用方延长,直到引用方解除对它的持有。 - 当两个对象之间通过强引用相互持有时,就会形成 强引用循环(Retain Cycle),这种情况下,即使它们不再需要对方,它们的引用计数也不会降到零,从而导致内存泄漏。
强引用的例子
1. 基本的强引用示例
假设你有两个对象:Person
和 Car
,Person
强引用 Car
,意味着只要 Person
存在,Car
就不会被释放。
class Person {var car: Car?
}class Car {var owner: Person?
}var john: Person? = Person()
var toyota: Car? = Car()john?.car = toyota
toyota?.owner = john
在这个例子中,john
对象强引用了 toyota
对象,同时 toyota
也强引用了 john
对象。由于两个对象互相持有对方,即使这两个对象的生命周期已经结束,它们的引用计数也不会变为零,因为它们彼此依赖,这就形成了 强引用循环。这会导致内存泄漏。
2. 解决强引用循环
可以通过使用 弱引用(weak
)来打破这种循环,确保当 Person
或 Car
不再需要时,它们能正确释放。
class Person {var car: Car?
}class Car {weak var owner: Person? // 使用 weak 避免强引用循环
}var john: Person? = Person()
var toyota: Car? = Car()john?.car = toyota
toyota?.owner = john
在这个例子中,owner
属性使用 weak
修饰符,这样当 john
被释放时,toyota
中的 owner
引用就会变为 nil
,从而打破强引用循环。
3. 强引用与内存管理的关系
当我们使用强引用时,ARC 会增加被引用对象的引用计数。下面是一个具体例子,说明如何影响内存管理:
class Dog {var name: Stringinit(name: String) {self.name = name}deinit {print("\(name) is being deinitialized")}
}var dog1: Dog? = Dog(name: "Buddy")
var dog2 = dog1 // dog2 强引用 dog1dog1 = nil // dog1 被置为 nil,但是 dog2 仍然持有 Buddy 的引用
在上面的代码中:
- 当
dog1
被赋值为nil
时,它不再持有Buddy
对象。 - 但是由于
dog2
仍然强引用着Buddy
,Buddy
对象并不会立即被释放。 Buddy
只有当dog2
也被置为nil
后,才会释放内存。
4. 常见场景:闭包中的强引用
闭包是导致强引用循环的一个常见场景。例如,下面的代码就会导致强引用循环:
class ViewController: UIViewController {var someProperty: String = "Hello"func setupClosure() {let closure = {print(self.someProperty) // 闭包捕获了 self}closure()}
}let vc = ViewController()
vc.setupClosure()
在上面的代码中,closure
捕获了 self
(即 ViewController
的实例),这意味着 ViewController
对象将始终被闭包引用,self
的引用计数不会变为零,导致 ViewController
永远不会被释放。为了避免这种情况,我们通常使用 弱引用 或 无主引用。
func setupClosure() {let closure: () -> Void = { [weak self] inprint(self?.someProperty ?? "")}closure()
}
使用 [weak self]
来捕获 self
,可以避免闭包对 ViewController
的强引用,从而避免强引用循环。
但需要注意的是,并不是所有的闭包都会产生强引用
不会产生强引用的情况
在闭包中使用 weak
或 unowned
主要是为了避免 强引用循环(retain cycle),尤其是当闭包捕获 self
时。是否需要使用 weak
或 unowned
取决于闭包与被捕获对象之间的生命周期关系。以下是几种情况,你可以根据这些情况来判断何时不需要使用 weak
或 unowned
。
1. 闭包的生命周期比捕获的对象短
如果闭包的生命周期比被捕获的对象短(即闭包不会持有对象,且对象的生命周期不会依赖于闭包),则无需使用 weak
或 unowned
。
示例:
- 闭包作为局部变量在方法内使用,并且被调用后立即销毁。
- 闭包没有持有对对象的引用,且被调用时对象已被销毁。
class MyClass {var value = 10func performAction() {// 闭包在执行后立即销毁,不会产生强引用循环let closure = {print(self.value)}closure()}
}let obj = MyClass()
obj.performAction()
在这个例子中,闭包在方法内部执行时,仅仅是一个局部变量。它不会在方法外部持有 self
,且方法执行完毕后,闭包就会被销毁,所以不需要使用 weak
或 unowned
来捕获 self
。
2. 闭包被存储在对象的属性中,并且对象的生命周期与闭包相同
如果闭包是对象的一个属性,并且该对象的生命周期与闭包相同(即闭包和对象一起销毁),则也不需要使用 weak
或 unowned
。
例如,闭包可能会作为一个委托回调、通知、或者是一些任务的完成闭包。如果该对象本身不会被提前销毁,闭包引用 self
是安全的,因为对象的生命周期会保持直到闭包不再需要。
示例:
class Task {var completion: (() -> Void)?func start() {completion = {print("Task is done")}}
}let task = Task()
task.start()
在这个例子中,Task
对象持有 completion
闭包,闭包本身不会引发强引用循环,因为 Task
对象会一直存在直到 completion
被销毁。这里没有强引用循环问题。
3. 捕获的对象不会导致内存泄漏
如果闭包捕获的对象是一个“非持有”对象(例如全局对象、常量、静态对象),或者闭包执行时被捕获的对象本身在执行完成后就会被销毁(比如临时创建的对象),则不需要使用 weak
或 unowned
。
示例:
class MyClass {func fetchData() {let completion: () -> Void = {print("Data fetched!")}completion()}
}let instance = MyClass()
instance.fetchData()
在这个例子中,completion
闭包没有捕获 self
,因此不会形成强引用循环。并且 completion
的生命周期非常短,执行完成后就会被销毁。
4. 闭包没有捕获 self
如果闭包本身不捕获 self
(即没有使用 [weak self]
或 [unowned self]
),且闭包本身不会导致引用循环,也可以不使用 weak
或 unowned
。这种情况下,闭包本身不会持有对象。
示例:
class MyClass {var name = "Hello"func fetchData() {let completion: () -> Void = {print("Data fetched!")}completion()}
}let instance = MyClass()
instance.fetchData()
这里 completion
不捕获 self
,所以 MyClass
对象不会因为闭包而延长生命周期。
总结:
我们需要注意闭包和实例是否会相互引用,还需要注意闭包和实例的生命周期
你不需要使用 weak
或 unowned
来捕获 self
的情况包括:
- 闭包的生命周期比捕获的对象短,例如临时的局部闭包。
- 闭包被存储在对象属性中,并且对象的生命周期与闭包相同。
- 闭包没有捕获
self
,且不涉及持有对象的引用。 - 捕获的对象不会导致内存泄漏,例如全局变量、常量等对象。