Swift 闭包: 捕获的奥秘

607 阅读5分钟

同学们在学习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.