Swift捕获列表


捕获列表,提到这个名词,可能很多做ios开发的人也不怎么了解。因为它在swift语法中,属于非常小众的特性,苹果文档也没有大篇幅讲过。但是它却在我们日常开发中经常用到。例如下面的用法:

clickBtn.bk_addEventHandler({[weak self] (_) in
    self?.view.removeFromSuperView()
}, for: .touchUpInside)

[weak self]这个用法很多人第一印象是说,要防止self循环引用。这往往是从oc语言继承过来的思想,虽然用法没错,但是思想还是有点偏差,[weak self]这个语法其实主要是为了配合闭包使用的,下面我们就来逐步分析下这个用法吧。

闭包特性

我们都知道swift语言的闭包非常强大,既可以用来作为函数的参数,也可以作为返回值。用过oc开发ios的应该都知道,oc的闭包中用到外部的变量,默认都是做copy的,如果需要改变外部变量的值,需要用到一个关键字__block,表明不要copy这个变量而是传递一个引用。但是swift语言已经移除了这个特性,因为oc闭包的这个用法有些反直觉,开发者往往在用这个特性的时候很容易忽略闭包中变量是copy,明显不符合swift人性化的设计理念。所以swift语言果断抛弃了这个用法,也就是默认情况下变量都是做的引用,例如我们看下面的代码:

var a = 0
var b = 0
let closure = { print(a, b) }

如果我们做如下调用:

a = 6
b = 7
closure()

控制台会输出6,7。这里明显看出来我们闭包中的变量没有被copy,而是直接引用的。

虽然这样不违反直觉了,但是有时候也会出现一些问题,例如下面的用法

var closureArray: [() -> ()] = []
var i = 0
for _ in 1...5 {
 closureArray.append { print(i) }
 i += 1
}

如果有人这样写代码,他一定是想要把计算的不同数字存下来,而不是输出下面的结果

closureArray[0]() // 5 😲
closureArray[1]() // 5 🤔
closureArray[2]() // 5 😨
closureArray[3]() // 5 😭
closureArray[4]() // 5 😱

事实上,确实会输出全部是5。如何才能实现输出不同的数字哪?下面就引出了我们要说的概念捕获列表

捕获列表

捕获列表就是在闭包中使用外部变量时,帮助闭包copy外部变量成为闭包的内部变量进行使用。下面就看下捕获列表的用法。

var c = 0
var d = 0
let closure: () -> () = { [c, d] in
 print(c, d)
}

用[]放置需要捕获的变量,就可以让这个变量做copy成为闭包的内部变量。下面我们输出下结果

c = 7
d = 8
closure()

可以看到结果仍然是0 0

捕获列表进阶

上面讲的捕获列表,相对来讲还是比较容易的,下面看一个用法可能有些迷惑,如果理解就能更充分明白swift中闭包中变量的使用。

var language = "Objc"
let code = { [language] in
    print(language)
}
language = "Swift"
code()
class Human {
    var lanuage = "Objc"
}
var human = Human()
let code = { [human] in
    print(human.lanuage)
}
human.lanuage = "Swift"
code()

上面这两段代码分别输出的结果时什么。如果你对捕获列表很了解的话,不难回答应该是Objc和Swift,前面不是说用了参数列表,闭包会copy变量作为内部的值么,为什么会输出不一样的结果哪?

其实前面说如果放到捕获列表中,闭包会copy外包变量的值,并不是很准确,准确的来讲是因为前面的例子变量是Int类型,因为Int和String都是Struct在赋值的时候,swift语言本身会对Struct的类型做copy操作。我们如果把参数列表拆解成代码,实际上可以表示为如下代码:

let language: String = language
let language: Human = language

而由于human是一个class,赋值并不会做copy,而只是强引用了此类的实例。下面我们就来拆解下[weak self]吧。

[weak self]由来

前面我们说过如果变量不放置到捕获列表中,闭包就不会copy或者强引用此变量。 swift文档有如下描述:

By default, a closure expression captures constants and variables from its surrounding scope with strong references to those values. You can use a capture list to explicitly control how values are captured in a closure.

划掉的部分是博客之前的描述不准确的点,感谢 @sinno93 提出了问题,更正为闭包不管使不使用捕获列表,都会强引用里面的变量。我们来验证下我们的想法,写如下代码:

class Human {
    var lanuage = "Objc"

    deinit {
        print("deinit")
    }
}

var human: Human! = Human()
let code = {
    print(human!.lanuage)
}
human.lanuage = "Swift"
human = nil
code()

这个代码可以运行下,会输出deinit,然后报如下的错误:

Fatal error: Unexpectedly found nil while unwrapping an Optional value

这是因为human这个类已经被释放了,如果我们改下代码如下:

let code = {[human] in
    print(human!.lanuage)
}

这时候你会发现输出Swift,并且deinit没有输出,准确的讲在展开捕获列表语法时,相当于又定义了一个临时变量let temp:Human = human,这个赋值本质上相当于humantemp这个变量强引用了。因为code这个闭包还没有释放,仍然持有temp这个变量,而如果我们不用捕获列表,会是同一个引用指向了human变量,当human = nil时,相当于闭包code和定义的human的引用都被释放了。为了证实这个问题,我们看下面的代码:

var human: Human! = Human()
print(CFGetRetainCount(human))
let code = {
    print(human!.lanuage)
}
print(CFGetRetainCount(human)) // 输出为2
var human: Human! = Human()
print(CFGetRetainCount(human))
let code = {[human] in
    print(human!.lanuage)
}
print(CFGetRetainCount(human)) // 输出为3

从上面的代码输出可以看出来,如果不用显式的捕获列表,输出human的引用为2,如果改成显式的捕获列表,会显示human的引用为3。所以本质上显式捕获列表会定义一个临时变量temp(为了便于理解,其实这个临时变量的名字仍然较human,只是在不同的代码scope内),当不用显式的捕获列表human = nil是会把引用计数值为0,如果用了显式的捕获列表,闭包中的临时变量temp不会被置位nil,所以不会崩溃。

再来看段代码如下:

class Human {
    var lanuage = "Objc"

    var block: (() -> Void)?

    deinit {
        print("deinit")
    }

    func recycle() -> () -> () {
        let code = {
            print(self.lanuage)
        }
        block = code
        return code
    }
}

var human: Human! = Human()
human.lanuage = "Swift"
human.recycle()
human = nil

然而你会发现控制台里面并没有输出deinit,human这个实例被循环引用了,为什么哪?其实swift语言,在类里面默认会把self变量放到捕获列表里面,上面的闭包的写法和下面本质上是等同的。

let code = { [self] in
    print(self.lanuage)
}

这时候我们终于引出了本文讨论的话题[weak self],本质上[weak self]如果拆解成代码的话应该如下:

weak var `self`: Human? = self

对self变量做弱引用声明,因为是弱引用,闭包在使用这个变量时就要定义为optional,因为有可能这个变量为空,所以我们更改上面的代码如下:

let code = { [weak self] in
    print(self?.lanuage)
}

再运行就可以看到控制台输出deinit,另外在控制self引用的时候还有个关键字unowned,这个关键字和weak的区别是:unowned同样不会强引用变量self,但是它不会改变变量的类型为optional,也就是说如果变量self被释放后,闭包中仍然使用的话会崩溃。所以在用unowned关键字的时候,要保证闭包在调用的时候,self不会被释放。

总结

虽说[weak self]在项目开发中经常使用,但是只知道是为了防止循环引用,至于为什么能防止循环引用,我们往往可能会回答,因为不用的话self变量会被强引用。这时候如果再多思考一些,你就可以刨出来捕获列表,这个swift不常用的语法。

如果你喜欢这篇文章,谢谢你的赞赏

图3

如有疑问请联系我