相信大家都知道,闭包使用不当,会造成引用循环,从而造成App的内存泄露。但是有时候,看起来会造成引用循环的代码实际上并没有造成引用循环。这个需要我们正确的区分。
【栗子1】请大家看看下面的代码,分析请看注释:
class ClassA {
let power =2
var transform: (Int ->Int)?
var dataArr = [1,2,3,4]
deinit {
print("ClassA实例被销毁")
}
}
var a: ClassA =ClassA() // ClassA实例的引用计数为1
a.transform = { numin
returna.power * num //闭包捕获a, 则ClassA实例的引用计数 增加1,现在为2 ;同时,a.transform指向该闭包,闭包的引用计数为1
}
a.transform!(5)
a = ClassA()<span style="font-family: Arial, Helvetica, sans-serif;"> //将a重新赋值。原来的ClassA实例的引用计数减一。原来的ClassA实例的引用计数为1。</span>
由此得出结论:运行的最后,原来的ClassA实例持有一个指向闭包的引用;闭包也持有一个指向原来ClassA实例的引用。从而造成引用循环。
似乎如此。如图:
图 1-1
但是,当我们把这段代码拿到playground上运行,结果却发现我们的判断是错误的——最先创建的那个ClassA实例被正常销毁了。为什么会这样呢?
原因就是,闭包捕获的是闭包外部变量的引用,是引用,是引用,重要的事情说3遍!如果那个变量本身是引用,那么,闭包捕获的就是那个引用的引用了。所以在这里,闭包捕获的是变量a的引用,并不是ClassA实例的引用。也就没有造成引用循环。如图
图 1-2
相信大家现在明白了吧。
【栗子2】接下来,大家看看这个代码:
class ClassA {
let power =2
lazyvar transform: (Int ->Int) = {num in num *self.power}
var dataArr = [1,2,3,4]
deinit {
print("ClassA实例被销毁")
}
}
var a =ClassA()
a.transform(8)
a =ClassA()
这段代码跟例子1的代码很相似。但是在闭包里用的是self。在playground上运行,发现最开始的ClassA实例没有被正常销毁,这是由于其真正造成了内存泄露。为什么呢?
self,代表当前的实例。所以,在闭包中使用了self,使得闭包捕获了当前实例的引用,从而持有了指向当前实例的引用。从而造成引用循环。如图
图 2-1
【栗子3】可能有人打印过闭包内外变量的地址,发现它们的地址是一样的。如图
图 3-1
当我们分别在闭包外和闭包内打印变量a的地址的时候,发现这两个的地址一样的。这个可以在inout关键字中一样。如图
图 3-2
swift 中,inout是将值传递给函数,在函数执行完后,将这个值替换掉原来的那个值。从而达到使用inout能起到引用传递修改值的效果。我们在函数内外都对实参的地址进行了打印,结果两者是一摸一样的。图3-1中,闭包捕获的参数跟外部的地址一样,估计是苹果做了些处理。
备注:本文下的Xcode的版本为7.3.1。