iOS开发之带你畅游闭包Closure --Swift

Qinz
在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的函数执行结束---------------------")
    }
  1. 我们来看看控制台输出了什么:很有趣是不是吗?你会发现最后输出的是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,希望我的文章对你有帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 86.复合 Cases 共享相同代码块的多个switch 分支 分支可以合并, 写在分支后用逗号分开。如果任何模式...
    无沣阅读 1,354评论 1 5
  • 作者:Olivier Halligon,原文链接,原文日期:2016-07-25译者:walkingway;校对:...
    梁杰_numbbbbb阅读 366评论 0 3
  • 闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代...
    穷人家的孩纸阅读 1,700评论 1 5
  • 闭包可以从定义它们的上下文中捕获和存储对任何常量和变量的引用。 这被称为关闭这些常量和变量。 Swift处理所有的...
    Joker_King阅读 589评论 0 2
  • 使用理由 虚拟主机 虚拟主机使用的是特殊的软硬件技术,它把一台运行在因特网上的服务器主机分成一台台“虚拟”的主机,...
    hopevow阅读 1,818评论 0 10