同学们在学习Swift闭包的时候,我想其中最大的难点就可能是捕获这个概念。什么时候必须用self
,什么时候可以不用self
,什么时候会造成循环应用,有时候我们会感到困惑。今天就让我们一起梳理下这些难点,认真学习完本文后,我相信你能掌握好闭包捕获。
捕获值和捕获列表
让我们先从两道测试题开始, 然后徐徐揭开闭包捕获的神秘面纱。
- 例1
var a = 0 var b = 0 let closure = { [b] in print(a,b) } a = 10 b = 10 closure()
- 例2
class C { var value = 0 } var c1 = C() var c2 = C() let closure = { [c1] in print(c1.value, c2.value) } c1.value = 10 c2.value = 10 closure()
如果你能快速得到两道测验题的答案,那么恭喜你,对于Swift闭包你已经入门了;如果你感到有些模棱两可,那么说明你还需要继续学习,至少应当阅读完这篇文章。相信你认真阅读完本文,你将彻底掌握Swift 闭包。
答案
:
例1 a
= 10, b
= 0; 例2 c1.value
= 10,c2.value
= 10
解答
:
例1: 我们对于变量b
使用捕获列表,由于b
是值类型,捕获的时候相当于let b = b
,此时你无法在闭包内修改b
常量, 之后给变量b
赋值10
,不会影响闭包中常量b
值。对于变量a
,我们没有使用捕获列表,那它就是按引用捕获,我们在闭包内或者闭包外都可以修改变量a
。
例2: 对于引用类型,对于在捕获列表中的变量c1
,和值类型一样,捕获的时候相当于let c1 = c1
,此时你也无法对c1
重新赋值,但由于c1
是引用类型,所以你可以修改它的属性value
。对于未加入捕获列表的c2
,我们可以在闭包内或者闭包外给它重新赋值(当然一般不会这样做),我们也可以修改它的属性。
到目前为止,大部分捕获的知识相信你应该掌握的差不多了,接下来我们回顾下解答中用到的几个概念。
闭包捕获
: 闭包可以捕获其定义时所在上下文中的常量和变量。闭包随后可以在其内部引用并修改这些常量和变量的值,即使定义这些常量和变量的原作用域已经不再存在。
捕获列表
:默认情况下,闭包表达式会以强引用的方式捕获其所在作用域中的常量和变量。你可以使用捕获列表显式地控制闭包中值的捕获方式。捕获列表中的条目在闭包创建时进行初始化。对于捕获列表中的每个条目,常量会初始化为其在外部作用域中同名常量或变量的值。
闭包和循环引用
前面提到,闭包表达式会以强引用的方式捕获其所在作用域中的常量和变量,而闭包是引用类型,那么这里就会有可能出现循环引用。
class CycleRefrence {
var name: String
init(name: String) {
self.name = name
}
lazy var greetingProvider: ()-> String = {
return "Hello" + self.name
}
}
let c = CycleRefrence(name: "yiniu")
print(c.greetingProvider())
分析
实例c
强引用了闭包greetingProvider
,而闭包强引用了self
,也就是实例c
, 这就行成了循环引用,导致内存泄漏。注意,如果我们没有加self
显示引用属性name
, 编译器会报错,因为编译器认为这边可能会存在循环引用。(感谢编译器)。
解决
那么我们如何打破这个环呢,swift 给了我们两种方法。一种是使用unowned
, 我们只要将[unowned self]
添加到捕获列表,就可以打破这个环
lazy var greetingProvider: ()-> String = { [unowned self] in
return "Hello" + name
}
还有一种方式使用weak
关键字,我们将[weak self]
添加到捕获列表,也可以打破这个环
lazy var greetingProvider: ()-> String = { [weak self] in
return "Hello" + (self?.name ?? ""
}
这两种方式相同点就是闭包不再强引用self
,也就是引用计数不会+1
, 当实例c
出了作用域时,由于没有任何实例强引用它,它会销毁,而闭包不再被强引用,也会被销毁,没有造成内存泄漏。
这两种方式也有些不同
unowned
表示捕获的对象是非可选类型,并且在闭包中直接使用。这种方式要求开发者确信闭包执行时对象仍然存在。如果对象被释放,而你使用了 unowned self
,尝试访问 self
会导致运行时崩溃(因访问已释放的对象)。
weak
表示捕获的对象是可选类型,它可能在闭包执行前就被释放掉。因此,在使用 weak self
时,self
是一个可选类型 (self?
),并且需要在使用前进行解包。
当然对于示例,这两种方式都可以,因为闭包执行时,实例c
还存在。
这里我们还有一种方式解决示例 的循环引用,我们可以使用捕获列表直接捕获属性name
lazy var greetingProvider: ()-> String = { [name] in
return "Hello" + name
}
这里没有引用self
,不会有循环引用。
结语
相信你如果阅读到这儿,我相信你已经入门了。在我们开发过程中,我们需要留心系统资源的使用,尤其是内存。当我们遇到self
时,就要留心了,这里是不是会造成循环引用。这里我给大家一个小提示,在可以不用self
的地方不要显示使用self
,充分利用编译器,这样我们只需要留心出现self
的地方。(再次感谢编译器开发人员)
纸上得来终觉浅,绝知此事要躬行,送给各位iOSer.