在Swift中引进了闭包Closure的概念,使用起来更加的方便和简洁了,下面让我们揭开它的神秘面纱,带你畅行它执行的详细过程,熟悉理解本文中的每个实例,你就对闭包有了深刻认识了,废话不多说,开始畅游~
一:首先我们创建一个Cat的类,声明一个name的常量和description的变量
class Cat{
let name:String
init(name:String) {
self.name = name
}
var description:String{return "<名字 \(name)>"}
deinit {
print("🐱对象被销毁")
}
}
1.写一个延迟执行的闭包函数,这里延迟主要是方便我们接下来的观察
func delay(seconds:Int,closure:@escaping ()->()){
print("当前时间为\(Date())")
DispatchQueue(label: "com.ys").asyncAfter(deadline: .now() + .seconds(seconds), execute: {
print("延迟执行所在的线程\(Thread.current)")
print("当前时间为\(Date())")
print("🐱")
closure()
})
}
2.我们来调用闭包函数
func demo1(){
let pokemon1 = Cat(name: "ys")
print("-----开始执行demo1的函数-----\(pokemon1)")
delay(seconds: 2, closure: {
print("-----执行闭包中-------\(pokemon1)")
})
print("当前所在的线程\(Thread.current)")
print("--------demo1的函数执行结束---------------------")
}
3.我们来看看控制台输出了什么
-----开始执行demo1的函数-----ClosureSamples.Cat
当前时间为2017-03-17 02:10:33 +0000
--------demo1的函数执行结束---------------------
当前时间为2017-03-17 02:10:35 +0000
🐱
-----执行闭包中-------ClosureSamples.Cat
🐱对象被销毁
4.分析如下
a:首先我们调用了demo1()方法,它输出”-----开始执行demo1函数------“的log,这个毋容置疑。
b:接下来demo1()里面又调用了delay(seconds:Int,closure:@escaping ()->())这个含有闭包参数的函数,说明下,这里有一个关键字escaping表示的是逃逸闭包,就是闭包中的变量可以被捕获在调用的地方引用,这里我们研究的是闭包执行的过程,所以并没有给闭包参数。
c:执行delay()这个函数,打印出了当前的时间,我们只看秒数,因为这里我们是延迟2秒执行 现在的秒数是33。
d:现在打印出”----demo1函数执行结束------“,估计在这里有些人容易懵逼,可能会想为什么不先执行DispatchQueue里面的代码打印出35秒这个当前时间呢?原因很简单,因为打印"----demo1函数执行结束------"代码在主线程中执行,而延迟执行的代码是在子线程执行的,如果你觉得还不理解,建议先看看这篇多线程的基础知识详解,点击这里跳转 。
e:对于步骤d理解后,接下来我们再来看,打印出35秒的时间,说明延迟2秒代码开始执行了,然后我们在DispatchQueue中开始执行闭包closure(),这时候代码跳转至demo1()函数的delay中开始执行闭包中的代码,打印出“-----执行闭包中-------”,我们可以看到此时这只还是活着的,当闭包代码执行完毕,接着就走了Cat类中的deinit
方法,这时就挂了。
5.我们来打印出当前的线程
func demo1(){
let pokemon1 = Cat(name: "ys")
print("-----开始执行demo1的函数-----\(pokemon1)")
delay(seconds: 0, closure: {
print("-----执行闭包中-------\(pokemon1)")
})
print("当前所在的线程\(Thread.current)")
print("--------demo1的函数执行结束---------------------")
}
func delay(seconds:Int,closure:@escaping ()->()){
print("当前时间为\(Date())")
DispatchQueue(label: "com.ys").asyncAfter(deadline: .now() + .seconds(seconds), execute: {
print("延迟执行所在的线程\(Thread.current)")
print("当前时间为\(Date())")
print("🐱")
closure()
})
}
6.控制台输出为
-----开始执行demo1的函数-----ClosureSamples.Cat
当前时间为2017-03-17 02:16:25 +0000
--------demo1的函数执行结束---------------------
延迟执行所在的线程<NSThread: 0x610000066bc0>{number = 3, name = (null)}
当前时间为2017-03-17 02:16:25 +0000
🐱
-----执行闭包中-------ClosureSamples.Cat
🐱对象被销毁
- 总结
对于上面闭包执行过程的总结:demo1() 函数执行完毕后,闭包才开始执行;并且2秒后当闭包被执行的时候 实例依然存活着。这是因为闭包捕获(强引用)了 变量:编译器发现在闭包内部引用了 变量,它会自动捕获该变量(默认是强引用),所以 的生命周期与闭包自身是一致的。因此,闭包有点像精灵球 ,只要你持有着精灵球闭包, 变量也就会在那里,不过一旦精灵球闭包被释放,引用的 也会被释放。例子中:一旦 GCD 执行完毕,闭包就会被释放,所以 的 deinit 方法也会被调用。值得注意的是 Swift 在闭包执行时才会取出捕获变量的值[^1],所以这里的性能消耗是很小的,我们可以认为它之前捕获的是变量的引用(或指针)。
二:接下来我们在demo1的基础上加上对变量进行赋值的代码,看第二个例子
func demo2(){
var pokemon2 = Cat(name: "王子🍡")
print("-----开始执行demo2的函数-----\(pokemon2.name)")
delay(seconds: 2, closure: {
print("-----执行闭包中-------\(pokemon2.name)")
})
pokemon2 = Cat(name: "公主👸")
print("--------demo2的函数执行结束---------------------")
}
1.我们看看控制台输出的内容
-----开始执行demo2的函数-----王子🍡
当前时间为2017-03-17 02:22:18 +0000
🐱对象被销毁
--------demo2的函数执行结束---------------------
延迟执行所在的线程<NSThread: 0x618000264ac0>{number = 3, name = (null)}
当前时间为2017-03-17 02:22:20 +0000
🐱
-----执行闭包中-------公主👸
🐱对象被销毁
2.分析
a 对于前两段输出内容肯定毋容置疑。
b 为什么到第三段直接输出了"对象被销毁"呢?是不是又有点懵逼了,不慌--带你看是什么原理,此处和demo1()不同的是我们将pokemon2由原来的“王子”重新赋值给了“公主”,此时引用发生了变化,所以“王子”顺利挂彩,“公主”接上,那么接下来主线程中的"--------demo2的函数执行结束---------------------"开始输出,接着延迟2秒至32,开始打印DispatchQueue中的代码,然后执行闭包,开始跳转至demo2()中的delay函数执行闭包里面的代码,打印出“-----执行闭包中-------公主”,我们可以看到此时“公主””还是活着的,当闭包代码执行完毕,接着就走了Cat类中的deinit方法,这时“公主”也挂了!
总结
对于二的总结:在创建完闭包之后修改了 pokemon 对象,闭包延迟2秒后执行(虽然此时已经脱离了 demo2() 函数的作用域),我们打印的结果是新的 pokemon 对象,而不是旧的!这是因为 Swift 默认捕获的是变量的引用:首先初始化一个值为 "王子" 的 pokemon 对象,接着修改该对象的值为 "公主",之前值为 "王子" 的对象由于没有其他变量强引用,所以会被释放。接着闭包等待2秒钟执行,打印捕获 "公主" 变量(引用)的内容,待闭包执行完毕“公主”也就被释放了。
三:我们来看一个值类型的闭包捕获过程
func demo3() {
var value = 200
print("-----开始执行demo3的函数-----\(value)")
delay(seconds: 1, closure: {
value = 10000
print("-----执行闭包中-------\(value)")
})
value = 230
print("--------demo3的函数执行结束---------------------")
}
1.我们来看看控制台输出了什么
-----开始执行demo3的函数-----200
当前时间为2017-03-17 02:24:55 +0000
--------demo3的函数执行结束---------------------
延迟执行所在的线程<NSThread: 0x6100000701c0>{number = 3, name = (null)}
当前时间为2017-03-17 02:24:56 +0000
🐱
-----执行闭包中-------10000
总结
理解了demo2也就理解demo3了,可以看出值类型和字符串类型执行的是一样的,所以针对于demo2的特性:对于值类型也是一样的,闭包打印了新的整型变量值——尽管整型变量是值类型!因为它捕获了变量的引用,而不是变量自身的内容!,如果捕获的是变量 var(而不是常量 let),你也可以在闭包中[^2]修改它的值
四:我们在demo3的基础上做一点点小小的改动
func demo4(){
var value = 100
print("-----开始执行demo4的函数-----\(value)")
delay(seconds: 1, closure: { [oldValue = value] in
print("-----执行闭包中-------\(oldValue)")
})
value = 800
print("--------demo4的函数执行结束---------------------")
}
- 我们来看看控制台输出了什么:很有趣是不是吗?你会发现最后输出的是100,这就是闭包一个很好的特征,可以拿到之前的值,我们在这里写了[oldValue = value]就可以拿到oldValue之前的值,也就是被赋值为800之前的值,此时的100是旧值,800是新的值。
-----开始执行demo4的函数-----100
当前时间为2017-03-17 02:29:26 +0000
--------demo4的函数执行结束---------------------
延迟执行所在的线程<NSThread: 0x618000074a40>{number = 3, name = (null)}
当前时间为2017-03-17 02:29:27 +0000
🐱
-----执行闭包中-------100
2.如果你想,800到哪里去了呢?没错,它就是此时的value,看打印输出
func demo4(){
var value = 100
print("-----开始执行demo4的函数-----\(value)")
delay(seconds: 1, closure: { [oldValue = value] in
print("-----执行闭包中-------\(oldValue)")
print("-----执行闭包中-------\(value)")
})
value = 800
print("--------demo4的函数执行结束---------------------")
}
-----开始执行demo4的函数-----100
当前时间为2017-03-17 02:30:51 +0000
--------demo4的函数执行结束---------------------
延迟执行所在的线程<NSThread: 0x60800006f0c0>{number = 3, name = (null)}
当前时间为2017-03-17 02:30:52 +0000
🐱
-----执行闭包中-------100
-----执行闭包中-------800
3.有人会想我能不能对oldValue再在闭包里面赋值呢?孩子你想多了---显然它是一个let类型啊!
五:根据以上,我们来看个稍微复杂的示例
func demo5() {
var pokemon = Cat(name: "王子 🍡")
print("-----开始执行demo4的函数-----\(pokemon.name)")
delay(seconds: 1, closure: { [pokemonCopy = pokemon] in
print("-----执行闭包中-------\(pokemonCopy.name)")
print("******执行闭包中*******\(pokemon.name)")
})
pokemon = Cat(name: "公主 👸")
print("--------demo5的函数执行结束---------------------")
}
1.我们来看看控制台的输出:经过上面熟悉了四个例子的前提,是不是很容易明白下面的输出结果了呢?如果还不明白,看我下面的分析。有点懵逼的就是为什么”王子 “被赋值为"公主 "之后"王子 "没有被销毁,原因就在于在闭包的参数中我们写了[pokemonCopy = pokemon],这样我们又对旧值有了引用,不会消失了吧?我们看看有这句和没有这句的区别 ,下面是对比:
-----开始执行demo4的函数-----王子 🍡
当前时间为2017-03-17 02:34:41 +0000
--------demo5的函数执行结束---------------------
延迟执行所在的线程<NSThread: 0x60000007a480>{number = 3, name = (null)}
当前时间为2017-03-17 02:34:42 +0000
🐱
-----执行闭包中-------王子 🍡
******执行闭包中*******公主 👸
🐱对象被销毁
🐱对象被销毁
不写[pokemonCopy = pokemon]
func demo5() {
var pokemon = Cat(name: "王子 🍡")
print("-----开始执行demo4的函数-----\(pokemon.name)")
delay(seconds: 1, closure: {
// print("-----执行闭包中-------\(pokemonCopy.name)")
print("******执行闭包中*******\(pokemon.name)")
})
pokemon = Cat(name: "公主 👸")
print("--------demo5的函数执行结束---------------------")
}
控制台输出
-----开始执行demo4的函数-----王子 🍡
当前时间为2017-03-17 02:36:07 +0000
🐱对象被销毁
--------demo5的函数执行结束---------------------
延迟执行所在的线程<NSThread: 0x610000260a80>{number = 3, name = (null)}
当前时间为2017-03-17 02:36:08 +0000
🐱
******执行闭包中*******公主 👸
🐱对象被销毁
分析
a ”王子 “被创造。
b 接着闭包捕获了 ”王子 “ 的拷贝(这里实际上是捕获了 pokemon 变量的值)。 所以当我们紧接着为 pokemon 变量赋新值 “公主 ” 后,“王子 ” 还没有被释放,依然被闭包所保留。
c 当我们离开 demo5 函数的作用域,”公主 “ 就被释放了,在方法内部 pokemon 变量自身只被一个强引用所保持,离开作用域强引用也就消失了。
d 稍后闭包执行时,打印了 "王子 “,这是因为在闭包创建时,捕获列表就捕获了 Pokemon。
e 最后 GCD 释放了闭包,由此可以证明闭包保持了"王子 “的引用。
六:好了,最后来看一个综合例子
1.先想想下面这段代码会怎么输出?
func demo6() {
var pokemon = Cat(name: "王子 🍡")
print("----- 初始化 pokemon 为 \(pokemon.name)")
delay(seconds: 2, closure: { [capturedPokemon = pokemon] in
print("closure 1 — 旧值: \(capturedPokemon.name)")
print("closure 1 — 新值: \(pokemon.name)")
pokemon = Cat(name: "公主 👸")
print("closure 1 - pokemon的值为 \(pokemon.name)")
})
pokemon = Cat(name: "爱心 ❤️")
print("****** pokemon 发生改变为 \(pokemon.name)")
delay(seconds: 2, closure: { [capturedPokemon = pokemon] in
print("closure 2 — 旧值: \(capturedPokemon.name)")
print("closure 2 — 新值: \(pokemon.name)")
pokemon = Cat(name: "青蛙 🐸")
print("closure 2 - pokemon的值为 \(pokemon.name)")
})
}
2.来~我们看看控制台输出结果:估计你看到又懵逼了,不急,我们来慢慢分析为什么是这样输出的,如果你理解了这个,说明闭包就真的已经深入理解了。
----- 初始化 pokemon 为 王子 🍡
当前时间为2017-03-17 02:41:00 +0000
****** pokemon 发生改变为 爱心 ❤️
当前时间为2017-03-17 02:41:00 +0000
延迟执行所在的线程<NSThread: 0x6000002624c0>{number = 4, name = (null)}
延迟执行所在的线程<NSThread: 0x61800007ca00>{number = 3, name = (null)}
当前时间为2017-03-17 02:41:02 +0000
当前时间为2017-03-17 02:41:02 +0000
🐱
closure 2 — 旧值: 爱心 ❤️
🐱
closure 1 — 新值: 爱心 ❤️
closure 1 — 旧值: 王子 🍡
closure 2 - pokemon的值为 青蛙 🐸
closure 1 - 新值: 青蛙 🐸
🐱对象被销毁
🐱对象被销毁
closure 1 - pokemon的值为 公主 👸
🐱对象被销毁
🐱对象被销毁
分析
- 1:输出”----- 初始化 pokemon 为 王子 “肯定是毋庸置疑的。
- 2:输出”当前时间为2017-03-16 08:43:40 +0000“也肯定完全理解。
- 3:输出”****** pokemon 发生改变为 爱心 ❤️“原因是这段输出是在主线程中执行,不懂点击这里先熟悉多线程知识 。
- 4:输出”当前时间为2017-03-16 08:43:40 +0000“说明开始进入第一次调用的delay方法中。
- 5:输出”延迟执行所在的线程{number = 3, name = (null)}“说明进入DispatchQueue开启子线程执行代码
- 6:为什么在第5步输出”延迟执行所在的线程{number = 3, name = (null)}“接着打印”延迟执行所在的线程{number = 5, name = (null)}“,因为此时进入到了我们第二次调用delay的方法中,”number = 3“和”number = 5“可以看出两次调用delay的方法是在不同的子线程中进行的异步并发任务,这点不明白,还是点击这里先熟悉多线程知识 。
- 7:输出”当前时间为2017-03-16 08:43:42 +0000“肯定也是毋庸置疑的。
- 8:接着又输出”当前时间为2017-03-16 08:43:42 +0000“,其实原理和5,6步是一样的,也就是它们执行的是并行任务。
- 9:接下来打印了一只”“,毋容置疑
- 10:这里开始懵逼?按照5,6和7,8的步骤规律,应该接下来再打印一只猫才对,怎么乱入了”closure 2 — 旧值: 爱心 ❤️“呢?其实这个还是并行任务的特点,我们再次运行一遍程序,发现输出结果如下:两只”“又同时输出了,其实发现它们就是打印一遍delay第一次调用的方法里面的代码,再打印一次delay第二次调用的方法里面的代码,交替进行,也没先后的。
----- 初始化 pokemon 为 王子 🍡
当前时间为2017-03-17 02:43:39 +0000
****** pokemon 发生改变为 爱心 ❤️
当前时间为2017-03-17 02:43:39 +0000
延迟执行所在的线程<NSThread: 0x608000071a80>{number = 3, name = (null)}
延迟执行所在的线程<NSThread: 0x610000073b80>{number = 4, name = (null)}
当前时间为2017-03-17 02:43:41 +0000
当前时间为2017-03-17 02:43:41 +0000
🐱
🐱
closure 1 — 旧值: 王子 🍡
closure 2 — 旧值: 爱心 ❤️
closure 1 — 新值: 爱心 ❤️
closure 2 — 新值: 爱心 ❤️
closure 1 - pokemon的值为 公主 👸
closure 2 - pokemon的值为 青蛙 🐸
🐱对象被销毁
🐱对象被销毁
🐱对象被销毁
🐱对象被销毁
11:接下来执行了closure()代码,回到delay的闭包中,一直从”closure 1 — 旧值: 王子 “到输出”closure 2 - pokemon的值为 青蛙 “都是交替进行,各自输出一次的。
12:为什么最后四只”“是一起挂的,因为我们在闭包里面都对旧值有了引用,也就是和第五个步骤的原理是一样的,所以都是等闭包代码执行完才被释放。
总结:Swift中的Closure还是很方便和有趣的,了解和熟悉了它执行的过程对于以后的开发非常有利,因为你会发现在OC的工程中我们使用了大量的Block,而Swift把它变得更加优雅了。
我是Qinz,希望我的文章对你有帮助。