闭包产生的强引用环
前面我们看到了强引用环是如何产生的,还知道了如何引入弱引用和无主引用来打破引用环。将一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如self.someProperty,或者调用了实例的某个方法,例如self.someMethod。这两种情况都导致了闭包使用self,从而产生了抢引用环。
因为诸如类这样的闭包是引用类型,导致了强引用环。当你把一个闭包赋值给某个属性时,你也把一个引用赋值给了这个闭包。实质上,这个之前描述的问题是一样的-两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。
Swift提供了一种优雅的方法来解决这个问题,我们称之为闭包占用列表(closuer capture list)。同样的,在学习如何避免因闭包占用列表产生强引用环之前,先来看看这个抢引用环是如何产生的。
下面的例子将会告诉你当一个闭包引用了self后是如何产生一个抢引用环的。本例顶一个一个名为HTMLElement的类,来建模HTML中的一个单独的元素:
- class HTMLElement {
- let name: String
- let text: String?
- @lazy var asHTML: () -> String = {
- if let text = self.text {
- return "<\(self.name)>\(text)</\(self.name)>"
- } else {
- return "<\(self.name) />"
- }
- }
- init(name: String,text: String? = nil) {
- self.name = name
- self.text = text
- deinit {
- println("\(name) is being deinitialized")
- }
除了上面的两个属性,HTMLElement还定义了一个lazy属性asHTML。这个属性引用了一个闭包,将name和text组合成HTML字符串片段。该属性是() -> String类型,就是“没有参数,返回String的函数”。
默认将闭包赋值给了asHTML属性,这个闭包返回一个代表HTML标签的字符串。如果text值存在,该标签就包含可选值text;或者不包含文本。对于段落,根据text是"some text"还是nil,闭包会返回"<p>some text</p>"或者"<p />"。
可以像实例方法那样去命名、使用asHTML。然而,因为asHTML终究是闭包而不是实例方法,如果你像改变特定元素的HTML处理的话,可以用定制的闭包来取代默认值。
注意:asHTML声明为lazy属性,因为只有当元素确实需要处理为HTML输出的字符串时,才需要使用asHTML。也就是说,在默认的闭包中可以使用self,因为只有当初始化完成以及self确实存在后,才能访问lazy属性。HTMLElement只有一个初始化函数,根据name和text(如果有的话)参数来初始化一个元素。该类也定义了一个deinitializer,当HTMLElement实例被销毁时,打印一条消息。
下面的代码创建一个HTMLElement实例并打印消息。
注意上面的paragraph变量定义为可选HTMLElement,因此我们可以赋值nil给它来演示强引用环。不幸的是,HTMLElement类产生了类实例和asHTML默认值的闭包之间的强引用环。如下图所示:
实例的asHTML属性持有闭包的强引用。
但是,闭包使用了self(引用了self.name和self.text),因此闭包占有了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样就产生了强引用环。(更多闭包哪占有值的信息,请参考Capturing Values)。
注意:虽然闭包多次使用了self,它只占有HTMLElement实例的一个强引用。如果设置paragraph为nil,打破它持有的HTMLElement实例的强引用,HTMLElement实例和它的闭包都不会被销毁,就因为强引用环:
paragraph = nil
解决闭包产生的强引用环
在定义闭包时同时定义占有列表作为闭包的一部分,可以解决闭包和类实例之间的强引用环。占有列表定义了闭包内占有一个或者多个引用类型的规则。和解决两个类实例间的强引用环一样,声明每个占有的引用为弱引用或无主引用,而不是强引用。根据代码关系来决定使用弱引用还是无主引用。注意:Swift有如下约束:只要在闭包内使用self的成员,就要用self.someProperty或者self.someMethod(而非只是someProperty或someMethod)。这可以提醒你可能会不小心就占有了self。
定义占有列表
占有列表中的每个元素都是由weak或者uNowned关键字和实例的引用(如self或someInstance)组成。每一对都在花括号中,通过逗号分开。占有列表放置在闭包参数列表和返回类型之前:
@lazy var someClosure: (Int,String) -> String = {
[uNowned self] (index: Int,stringToProcess: String) -> String in
// closure body goes here
如果闭包没有指定参数列表或者返回类型(可以通过上下文推断),那么占有列表放在闭包开始的地方,跟着是关键字in:
相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。利用这个特性,我们可以在闭包内检查他们是否存在。
@lazy var someClosure: () -> String = {
[uNowned self] in
}
弱引用和无主引用
当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。利用这个特性,我们可以在闭包内检查他们是否存在。
注意:如果占有的引用绝对不会置为nil,应该用无主引用,而不是弱引用。前面提到的HTMLElement例子中,无主引用是正确的解决强引用的方法。这样编码HTMLElement类来避免强引用环:
上面的HTMLElement实现和之前的实现相同,只是多了占有列表。这里,占有列表是[uNowned self],代表“用无主引用而不是强引用来占有self”。
和之前一样,我们可以创建并打印HTMLElement实例:
使用占有列表后引用关系如下图所示:
这一次,闭包以无主引用的形式占有self,并不会持有HTMLElement实例的强引用。如果赋值paragraph为nil,HTMLElement实例将会被销毁,并能看到它的deinitializer打印的消息。
和之前一样,我们可以创建并打印HTMLElement实例:
println(paragraph!.asTHML())
这一次,闭包以无主引用的形式占有self,并不会持有HTMLElement实例的强引用。如果赋值paragraph为nil,HTMLElement实例将会被销毁,并能看到它的deinitializer打印的消息。
paragraph = nil
// 打印"p is being deinitialized"
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。