闭包产生的强引用环

闭包产生的强引用环

前面我们看到了强引用环是如何产生的,还知道了如何引入弱引用和无主引用来打破引用环。

一个闭包赋值给类实例的某个属性,并且这个闭包使用了实例,这样也会产生强引用环。这个闭包可能访问了实例的某个属性,例如self.someProperty,或者调用了实例的某个方法,例如self.someMethod。这两种情况都导致了闭包使用self,从而产生了抢引用环。

因为诸如类这样的闭包是引用类型,导致了强引用环。当你把一个闭包赋值给某个属性时,你也把一个引用赋值给了这个闭包。实质上,这个之前描述的问题是一样的-两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个类实例,另一个是闭包。

Swift提供了一种优雅的方法解决这个问题,我们称之为闭包占用列表(closuer capture list)。同样的,在学习如何避免因闭包占用列表产生强引用环之前,先来看看这个抢引用环是如何产生的。

下面的例子将会告诉你当一个闭包引用了self后是如何产生一个抢引用环的。本例顶一个一个名为HTMLElement的类,来建模HTML中的一个单独的元素:
    
    
  1. class HTMLElement {
  2. let name: String
  3. let text: String?
  4. @lazy var asHTML: () -> String = {
  5. if let text = self.text {
  6. return "<\(self.name)>\(text)</\(self.name)>"
  7. } else {
  8. return "<\(self.name) />"
  9. }
  10. }
  11. init(name: String,text: String? = nil) {
  12. self.name = name
  13. self.text = text
  14. deinit {
  15. println("\(name) is being deinitialized")
  16. }
类HTMLElement定义了一个name属性来表示这个元素的名称,例如代表段落的"p",或者代表换行的"br";以及一个可选属性text,用来设置HTML元素的文本。

除了上面的两个属性,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实例并打印消息。
var paragraph: HTMLElement? = HTMLElement(name: "p",text: "hello,world")
  • println(paragraph!.asHTML())
  • // 打印"<p>hello,world</p>"
  • 注意上面的paragraph变量定义为可选HTMLElement,因此我们可以赋值nil给它来演示强引用环。
    不幸的是,HTMLElement类产生了类实例和asHTML认值的闭包之间的强引用环。如下图所示:


    实例的asHTML属性持有闭包的强引用。

    但是,闭包使用了self(引用了self.name和self.text),因此闭包占有了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样就产生了强引用环。(更多闭包哪占有值的信息,请参考Capturing Values)。
    注意:虽然闭包多次使用了self,它只占有HTMLElement实例的一个强引用。
    如果设置paragraph为nil,打破它持有的HTMLElement实例的强引用,HTMLElement实例和它的闭包都不会被销毁,就因为强引用环:
    paragraph = nil
    注意HTMLElementdeinitializer中的消息并没有别打印,印证了HTMLElement实例并没有被销毁。

    解决闭包产生的强引用环

    在定义闭包时同时定义占有列表作为闭包的一部分,可以解决闭包和类实例间的强引用环。占有列表定义了闭包内占有一个或者多个引用类型的规则。和解决两个类实例间的强引用环一样,声明每个占有的引用为弱引用或无主引用,而不是强引用。根据代码关系来决定使用弱引用还是无主引用。
    注意: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:
    @lazy var someClosure: () -> String = {
  • [uNowned self] in
  • }
  • 弱引用和无主引用

    当闭包和占有的实例总是互相引用时并且总是同时销毁时,将闭包内的占有定义为无主引用。

    相反的,当占有引用有时可能会是nil时,将闭包内的占有定义为弱引用弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。利用这个特性,我们可以在闭包内检查他们是否存在。
    注意:如果占有的引用绝对不会置为nil,应该用无主引用,而不是弱引用
    前面提到的HTMLElement例子中,无主引用是正确的解决强引用的方法。这样编码HTMLElement类来避免强引用环:
    上面的HTMLElement实现和之前的实现相同,只是多了占有列表。这里,占有列表是[uNowned self],代表“用无主引用而不是强引用来占有self”。
    和之前一样,我们可以创建并打印HTMLElement实例:
    println(paragraph!.asTHML())
  • 使用占有列表后引用关系如下图所示:

    这一次,闭包以无主引用的形式占有self,并不会持有HTMLElement实例的强引用。如果赋值paragraph为nil,HTMLElement实例将会被销毁,并能看到它的deinitializer打印的消息。
    paragraph = nil
  • // 打印"p is being deinitialized"
  • 相关文章

    软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘...
    现实生活中,我们听到的声音都是时间连续的,我们称为这种信...
    前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿...
    【Android App】实战项目之仿抖音的短视频分享App(附源码和...
    前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至...
    因为我既对接过session、cookie,也对接过JWT,今年因为工作...