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閱讀 229,327評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,249評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內容

  • 86.復合 Cases 共享相同代碼塊的多個switch 分支 分支可以合并, 寫在分支后用逗號分開。如果任何模式...
    無灃閱讀 1,405評論 1 5
  • 作者:Olivier Halligon,原文鏈接,原文日期:2016-07-25譯者:walkingway;校對:...
    梁杰_numbbbbb閱讀 383評論 0 3
  • 閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代...
    窮人家的孩紙閱讀 1,726評論 1 5
  • 閉包可以從定義它們的上下文中捕獲和存儲對任何常量和變量的引用。 這被稱為關閉這些常量和變量。 Swift處理所有的...
    Joker_King閱讀 596評論 0 2
  • 使用理由 虛擬主機 虛擬主機使用的是特殊的軟硬件技術,它把一臺運行在因特網上的服務器主機分成一臺臺“虛擬”的主機,...
    hopevow閱讀 1,840評論 0 10