本文主要参考
- The Swift Programming Language 中文版——闭包
- The Swift Programming Language 中文版——闭包引起的循环强引用
- onevcat的文章内存管理,WEAK 和 UNOWNED
1.循环强引用
由于Swift采用ARC(引用计数)来进行内存管理,开发者不必再去手动释放内存,但是如果代码产生了循环强引用,对象的引用计数无法归零,那么系统便无法自动释放对象,从而产生内存泄漏。
1.1 对象的循环强引用
我们假设两个类 类1、类2,这两个类中都有一个类类型的存储属性:
// 代码1.1
class 类1{
var 对象:类2!
init(){
print("对象1初始化!")
}
deinit{
print("对象1释放!")
}
}
class 类2{
var 对象:类1!
init(){
print("对象2初始化!")
}
deinit{
print("对象2释放!")
}
}
然后我们对着两个类分别实例化两个对象,并让这两个对象彼此互相持有:
// 代码1.2
var 对象1:类1? = 类1()
var 对象2:类2? =类2()
对象1?.对象 = 对象2
对象2?.对象 = 对象1
对象1 = nil
对象2 = nil
这样就会造成一个对象间的循环引用。最后,我们将这两个对象进行释放。发现控制台输出结果为:
对象1初始化!
对象2初始化!
而这两个对象并没有被释放,造成内存泄漏!
1.2 闭包的循环强引用
Swift中,闭包会自动捕捉变量,当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时。这个闭包体中可能访问了实例的某个属性,或者闭包中调用了实例的某个方法。这两种情况都导致了闭包“捕获”self,从而产生了循环强引用。
例如:
// 代码1.3
class 类3{
let name = "老王"
lazy var 闭包:(Void -> Void)? = {
var name1 = self.name
var name2 = self.getName()
print(name1)
print(name2)
}
init(){
print("对象3初始化!")
}
func getName()->String{
return "老王"
}
deinit{
print("对象3释放!")
}
}
var 对象3:类3? = 类3()
对象3.闭包!()
对象3 = nil
在类3中,由于闭包中调用了实例的属性以及方法,从而导致闭包捕获了self,产生了循环强引用,造成了内存泄漏。
2.如何避免循环强引用
Swift中防止出现循环强引用主要有两种方法:
1. 代码显式调用,打破循环强引用
2. 使用weak、unowned关键字,用弱引用或无主引用打破循环强引用
2.1代码显式调用,打破循环强引用
这种方法需要开发者在使用完对象或闭包后,主动释放所持有的其他对象的强引用,从而打破循环,让系统回收内存。
例如,我们可以将代码1.2修改为:
// 代码2.1
var 对象1:类1? = 类1()
var 对象2:类2? =类2()
对象1?.对象 = 对象2
对象2?.对象 = 对象1
//主动释放所持有的强引用对象
对象1?.对象 = nil
对象2?.对象 = nil
对象1 = nil
对象2 = nil
对于代码1.3可以主动释放对象中的闭包:
//代码2.2
var 对象3:类3? = 类3()
对象3?.闭包!()
对象3?.闭包 = nil
对象3 = nil
这样就可以避免内存泄漏,但是这种方式有悖于ARC设计的初衷,不推荐使用。
2.2使用weak、unowned关键字,用弱引用或无主引用打破循环强引用
2.2.1 weak、unowned关键字
Swift中,声明属性或者变量时,在前面加上weak关键字表明这是一个弱引用,弱引用不会对其引用的实例保持强引用,不会增加引用对象的引用计数,因而不会阻止 ARC 销毁被引用的实例。因为弱引用可以没有值,因此你必须将每一个弱引用声明为可选类型。弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC 会在引用的实例被销毁后自动将其赋值为nil。你可以像其他可选值一样,检查弱引用的值是否存在,你将永远不会访问已销毁的实例的引用。
与weak关键字类似,在属性或变量前面加上unowned关键字表明这是一个无主引用,无主引用也不会对其引用的实例保持强引用,从而避免出现循环强引用。然而与weak关键字不同的是,即使引用的对象已经被销毁,无主引用仍然存在,这时候如果强制使用引用会导致程序的崩溃。对于这两个关键字的选择,建议如果能够确保在引用的使用过程中对象始终存在,则尽量使用unowned,否则建议使用更为安全的weak关键字。
2.2.2 weak、unowned关键字在属性或变量上的使用
weak、unowned关键字在属性或变量上的使用比较简单,代码如下:
// 代码2.3
//weak关键字
class 类1{
weak var 对象:类2!
init(){
print("对象1 初始化!")
}
deinit{
print("对象1 释放!")
}
}
// unowned关键字
class 类1{
unowned var 对象:类2
init(对象2:类2){
对象 = 对象2
print("对象1 初始化!")
}
deinit{
print("对象1 释放!")
}
}
2.2.3 weak、unowned关键字在闭包中的使用
weak、unowned关键字在闭包中的使用主要有两种方式,首先便是在闭包定义之前先用weak、unowned关键字引用对象,从而让闭包捕获弱引用或无主引用对象,从而保证不会出现循环强引用:
class 类3{
let name = "老王"
var closure:(Void -> Void)?
init(){
print("对象3初始化!")
unowned let myself = self
closure = {
//捕获myself无主引用
var name = myself.name
name = myself.getName()
print(name)
}
}
}
这种方式比较麻烦,不推荐使用。Swift提供了另外一个简便的方式:自定义捕获列表。开发者在定义闭包的时候可以首先定义需要捕获变量类型为weak或unowned,从而防止出现循环强引用:
class 类3{
let name = "老王"
lazy var closure:(Void -> Void)? = {
//自定义捕获列表
[unowned self] in
var name = self.name
name = self.getName()
print(name)
}
}
这样,闭包中再使用self的时候便不会出现强引用了。
注:文中如有任何错误,请各位批评指正!
网友评论