閉包, 一個(gè)捕獲了全局上下文的常量或者變量的函數(shù)
。閉包
在實(shí)現(xiàn)上是一個(gè)結(jié)構(gòu)體
,它存儲(chǔ)了一個(gè)函數(shù)
(通常是其入口地址)和一個(gè)關(guān)聯(lián)的環(huán)境(相當(dāng)于一個(gè)符號(hào)查找表
)。
一、閉包的使用
全局函數(shù) 是一種特殊的閉包,定義一個(gè)全局函數(shù),只是當(dāng)前的全局函數(shù)并不捕獲值。
func test(){
print("test")
}
函數(shù)閉包:下面的函數(shù)是一個(gè)閉包,函數(shù)中的incrementer
是一個(gè)內(nèi)嵌函數(shù)
,可以從makeIncrementer
中捕獲變量runningTotal
。
func makeIncrementer() -> () -> Int{
var runningTotal = 10
//內(nèi)嵌函數(shù),也是一個(gè)閉包
func incrementer() -> Int{
runningTotal += 1
return runningTotal
}
return incrementer
}
let incre = makeIncrementer()
print(incre())
閉包表達(dá)式 / 匿名函數(shù):下面是一個(gè)閉包表達(dá)式
,即一個(gè)匿名函數(shù)
,而且是從上下文中捕獲變量和常量。
//閉包表達(dá)式
{ (param) -> ReturnType in
//方法體
}
閉包表達(dá)式特點(diǎn)
:是一個(gè)匿名函數(shù)
,所有代碼都在花括號(hào){}內(nèi),參數(shù)和返回類型
在in關(guān)鍵字
之前,in關(guān)鍵字
之后是主體內(nèi)容(類似方法體)。
尾隨閉包
當(dāng)閉包作為函數(shù)的最后一個(gè)參數(shù)
,如果當(dāng)前的閉包表達(dá)式很長(zhǎng),我們可以通過尾隨閉包的書寫方法來提高代碼的可讀性。
//閉包表達(dá)式作為函數(shù)的最后一個(gè)參數(shù)
func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int)
-> Bool) -> Bool{
return by(a, b, c)
}
//常規(guī)寫法
test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
return (item1 + item2 < item3)
})
//尾隨閉包寫法
test(10, 20, 30) { (item1, item2, item3) -> Bool in
return (item1 + item2 < item3)
}
array.sorted
就是一個(gè)尾隨閉包,且這個(gè)函數(shù)就只有一個(gè)參數(shù)
,如下所示:
//array.sorted就是一個(gè)尾隨閉包
var array = [1, 2, 3]
//1、完整寫法
array.sorted { (item1: Int, item2: Int) -> Bool in return item1 < item2}
//2、省略參數(shù)類型:通過array中的參數(shù)推斷類型
array.sorted { (item1, item2) -> Bool in return item1 < item2}
//3、省略參數(shù)類型 + 返回值類型:通過return推斷返回值類型
array.sorted { (item1, item2) in return item1 < item2}
//4、省略參數(shù)類型 + 返回值類型 + return關(guān)鍵字:?jiǎn)伪磉_(dá)式可以隱士表達(dá),即省略return關(guān)鍵字
array.sorted { (item1, item2) in item1 < item2}
//5、參數(shù)名稱簡(jiǎn)寫
array.sorted {return $0 < $1}
//6、參數(shù)名稱簡(jiǎn)寫 + 省略return關(guān)鍵字
array.sorted {$0 < $1}
//7、最簡(jiǎn):直接傳比較符號(hào)
array.sorted (by: <)
閉包的有點(diǎn)
:
利用上下文推斷參數(shù)和返回類型
;
單表達(dá)式
可以隱式返回,省略return
關(guān)鍵字;
參數(shù)名稱可以直接使用簡(jiǎn)寫(如$0
,$1
,元組的$0.0
);
尾隨閉包可以更簡(jiǎn)潔的表達(dá)。
二、變量捕獲
func makeIncrementer() -> () -> Int{
var runningTotal = 10
//內(nèi)嵌函數(shù),也是一個(gè)閉包
func incrementer() -> Int{
runningTotal += 1
return runningTotal
}
return incrementer
}
let incre = makeIncrementer()
print(incre())
print(incre())
print(incre())
//打印結(jié)果:11 12 13
結(jié)果為什么是累加
的:因?yàn)閮?nèi)嵌函數(shù)捕獲了runningTotal
,不再是單純的一個(gè)變量了。
換一種方式:
print(makeIncrementer()())
print(makeIncrementer()())
print(makeIncrementer()())
//打印結(jié)果:11 11 11
老司機(jī)應(yīng)該能猜到結(jié)果,下面我們深入分析一下原因。
2.1 SIL
SIL命令:swiftc -emit-sil ${SRCROOT}/SwiftTest/main.swift | xcrun swift-demangle > ./main.sil && open main.sil
1、通過
alloc_box
申請(qǐng)了一個(gè)堆
上的空間,并將引用計(jì)數(shù)地址給了RunningTotal
,將變量存儲(chǔ)到堆
上;2、通過
project_box
從堆上取出變量;3、將取出的變量交給
閉包
進(jìn)行調(diào)用。結(jié)論
:捕獲值的本質(zhì)是將變量存儲(chǔ)到堆上
。
LLDB:
斷點(diǎn)
,看到在makeIncrementer
方法內(nèi)部調(diào)用了swift_allocObject
方法。
總結(jié):
1、一個(gè)閉包能夠從上下文捕獲已經(jīng)定義的常量和變量,并且能夠在其函數(shù)體內(nèi)引用和修改這些值,即使這些定義的常量和變量的原作用域不存在;
2、修改捕獲值實(shí)際是修改堆區(qū)中的value值
;
3、當(dāng)每次重新執(zhí)行當(dāng)前函數(shù)時(shí),都會(huì)重新創(chuàng)建
內(nèi)存空間。
三、逃逸閉包 & 非逃逸閉包
逃逸閉包
,當(dāng)閉包作為一個(gè)實(shí)際參數(shù)傳遞給一個(gè)函數(shù)時(shí),并且是在函數(shù)返回之后
調(diào)用,我們就說這個(gè)閉包逃逸了。
當(dāng)聲明一個(gè)接受閉包作為形式參數(shù)的函數(shù)時(shí),可以在形式參數(shù)前寫@escaping
來明確閉包是允許逃逸的。
- 如果用
@escaping
修飾閉包后,我們必須顯示的在閉包中使用self
; -
swift3.0
之后,系統(tǒng)默認(rèn)閉包參數(shù)就是被@nonescaping
修飾。image.png
逃逸閉包的使用場(chǎng)景,①延遲調(diào)用,②作為屬性存儲(chǔ),在后面進(jìn)行調(diào)用。
延遲調(diào)用
1、在延遲方法
中調(diào)用逃逸閉包
class Animal {
//定義一個(gè)閉包屬性
var complitionHandler: ((Int)->Void)?
//函數(shù)參數(shù)使用@escaping修飾,表示允許函數(shù)返回之后調(diào)用
func makeIncrementer(amount: Int, handler: @escaping (Int)->Void){
var runningTotal = 0
runningTotal += amount
//賦值給屬性
self.complitionHandler = handler
//延遲調(diào)用
DispatchQueue.global().asyncAfter(deadline: .now()+0.1) {
print("逃逸閉包延遲執(zhí)行")
handler(runningTotal)
}
print("函數(shù)執(zhí)行完了")
}
func doSomething(){
self.makeIncrementer(amount: 10) {
print($0)
}
}
deinit {
print("Animal deinit")
}
}
var t = Animal()
t.doSomething()
sleep(2)
//打印結(jié)果:
//函數(shù)執(zhí)行完了
//逃逸閉包延遲執(zhí)行
//10
當(dāng)前方法執(zhí)行的過程中不會(huì)等待閉包執(zhí)行完成后再執(zhí)行,而是直接返回,所以當(dāng)前閉包的生命周期要比方法長(zhǎng)
。
作為屬性
當(dāng)閉包作為
存儲(chǔ)屬性
時(shí),主要有以下幾點(diǎn)說明:
1、定義一個(gè)閉包屬性;
2、在方法中對(duì)閉包屬性進(jìn)行賦值;
3、在合適的時(shí)機(jī)調(diào)用(與業(yè)務(wù)邏輯相關(guān))。
觀察上一段代碼,complitionHandler
作為Animal的屬性
,是在方法makeIncrementer
調(diào)用完成后才會(huì)調(diào)用,這時(shí),閉包的生命周期要
比當(dāng)前方法的生命周期長(zhǎng)
。
逃逸閉包 vs 非逃逸閉包 區(qū)別
-
非逃逸閉包:一個(gè)接受
閉包作為參數(shù)
的函數(shù),閉包是在這個(gè)函數(shù)結(jié)束前
被調(diào)用,即可以理解為閉包是在函數(shù)作用域結(jié)束前被調(diào)用。
1.1不會(huì)產(chǎn)生循環(huán)引用
,因?yàn)殚]包的作用域在函數(shù)作用域內(nèi),在函數(shù)執(zhí)行完成后,就會(huì)釋放閉包捕獲的所有對(duì)象;
1.2 針對(duì)非逃逸閉包,編譯器會(huì)做優(yōu)化:省略內(nèi)存管理調(diào)用
;
1.3 非逃逸閉包捕獲的上下文保存在棧上
,而不是堆上(官方文檔說明)。 -
逃逸閉包:一個(gè)接受
閉包作為參數(shù)
的函數(shù),逃逸閉包可能會(huì)在函數(shù)返回之后
才被調(diào)用,即閉包逃離了函數(shù)的作用域。
2.1 可能會(huì)產(chǎn)生循環(huán)引用
,因?yàn)樘右蓍]包中需要顯式
的引用self
(猜測(cè)其原因是為了提醒開發(fā)者,這里可能會(huì)出現(xiàn)循環(huán)引用了),而self可能是持有閉包變量的(與OC中block
的的循環(huán)引用類似);
2.2 一般用于異步函數(shù)的返回,例如網(wǎng)絡(luò)請(qǐng)求。 -
使用建議
:如果沒有特別需要,開發(fā)中使用非逃逸閉包是有利于內(nèi)存優(yōu)化
的,所以蘋果把閉包區(qū)分為兩種,特殊情況時(shí)再使用逃逸閉包。 -
總結(jié)
:主要區(qū)別就是調(diào)用時(shí)機(jī)和內(nèi)存管理不同
。
四、自動(dòng)閉包
自動(dòng)閉包
是一種自動(dòng)創(chuàng)建的閉包
,用于包裝傳遞給函數(shù)作為參數(shù)
的表達(dá)式。這種閉包不接受任何參數(shù)
,當(dāng)它被調(diào)用的時(shí)候,會(huì)返回被包裝在其中的表達(dá)式的值。這種便利語法讓你能夠省略閉包
的花括號(hào)
,用一個(gè)普通的表達(dá)式來代替顯式的閉包
。
有下面一個(gè)例子,當(dāng)condition
為true
時(shí),會(huì)打印錯(cuò)誤信息,即,如果是false
,當(dāng)前條件不會(huì)執(zhí)行。
//1、condition為false時(shí),當(dāng)前條件不會(huì)執(zhí)行
func debugOutPrint(_ condition: Bool, _ message: String){
if condition {
print("cjl_debug: \(message)")
}
}
debugOutPrint(true, "Application Error Occured")
如果字符串
是在某個(gè)業(yè)務(wù)邏輯中獲取的
func debugOutPrint(_ condition: Bool, _ message: String){
if condition {
print("animal_debug: \(message)")
}
}
func doSomething() -> String{
print("doSomething")
return "Network Error Occured"
}
//如果傳入true
debugOutPrint(true, doSomething())
//打印結(jié)果:animal_debug: Network Error Occured
//如果傳入false
debugOutPrint(false, doSomething())
//打印結(jié)果:doSomething
此時(shí),無論是傳入true
還是false
,當(dāng)前的方法doSomething
都會(huì)執(zhí)行,如果這個(gè)方法是一個(gè)非常耗時(shí)
的操作,這里就會(huì)造成一定的資源浪費(fèi)。所以為了避免這種情況,需要將當(dāng)前參數(shù)修改為一個(gè)閉包。
修改:將message
參數(shù)修改成一個(gè)閉包,需要傳入的是一個(gè)函數(shù)。即,需要時(shí)在調(diào)用
,延緩
了方法的調(diào)用時(shí)機(jī)。
//3、為了避免資源浪費(fèi),將當(dāng)前參數(shù)修改成一個(gè)閉包
func debugOutPrint(_ condition: Bool, _ message: () -> String){
if condition {
print("animal_debug: \(message())")
}
}
func doSomething() -> String{
print("doSomething")
return "Network Error Occured"
}
debugOutPrint(true, doSomething)
如果這里既可以傳string,也可以傳閉包,可以實(shí)現(xiàn)嗎?
可以通過@autoclosure
將當(dāng)前的閉包聲明成一個(gè)自動(dòng)閉包
,不接收任何參數(shù)
,返回值是當(dāng)前內(nèi)部表達(dá)式的值。所以當(dāng)傳入一個(gè)String時(shí),其實(shí)就是將String放入一個(gè)閉包表達(dá)式中
,在調(diào)用的時(shí)候返回。
//4、將當(dāng)前參數(shù)修改成一個(gè)閉包,并使用@autoclosure聲明成一個(gè)自動(dòng)閉包
func debugOutPrint(_ condition: Bool, _ message: @autoclosure() -> String){
if condition {
print("cjl_debug: \(message())")
}
}
func doSomething() -> String{
print("doSomething")
return "Network Error Occured"
}
//使用1:傳入函數(shù)
debugOutPrint(true, doSomething())
//使用2:傳入字符串
debugOutPrint(true, "Application Error Occured")
debugOutPrint(true, "Application Error Occured")
這句代碼等價(jià)于:
//相當(dāng)于用{}包裹傳入的對(duì)象,然后返回{}內(nèi)的值
{
//表達(dá)式里的值
return "Network Error Occured"
}