在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,希望我的文章對你有幫助。