Swift閉包的基礎筆者已經寫了一篇,如果你還不是很清楚基本使用,建議先看基礎篇,如果已經會了,那直接跳過,一起學習閉包進階篇吧
逃逸閉包與非逃逸閉包
逃逸閉包概念:當一個閉包作為參數傳到一個函數中,但是這個閉包在函數返回之后才被執行,我們稱該閉包從函數中逃逸。
非逃逸閉包概念:當一個閉包作為參數傳到一個函數中,并且這個閉包在函數沒有返回之前就執行完了,我們稱該閉包為非逃逸閉包。
舉個例子說明這個問題
class ViewController: UIViewController {
var x = 10
var callBackArray: [() -> Void] = []
// 逃逸閉包
func testEscapingClosure(callBack: @escaping () -> Void) {
callBackArray.append(callBack)
}
// 非逃逸閉包
func testNoEscapingClosure(closure: () -> Void) {
closure()
}
override func viewDidLoad() {
super.viewDidLoad()
testEscapingClosure { [unowned self] () -> Void in
self.x = 100 // 逃逸閉包需要顯式的寫出self
}
testNoEscapingClosure {
x = 200 // 非逃逸閉包無需顯式的寫出self
}
print(x) // 200
callBackArray.first?() // 修改了x
print(x) // 100
DispatchQueue.main.async {
self.x = 1 // 逃逸閉包需要顯式的寫出self
}
}
}
為什么逃逸閉包需要顯式的寫self,而非逃逸閉包可以省略不寫?分析這個問題我們分兩步分析:
1 非逃逸閉包為什么可以不用寫self
當執行testNoEscapingClosure函數的時候,函數體中創建了一個局部變量引用著closure閉包,閉包中要訪問對象self中的成員變量首先需要引用self,此時在內存中的關系如圖所示,并不會出現循環引用,當函數執行結束,引用著閉包的成員變量會被釋放,從而圖示中的虛線斷開了,閉包引用計數為0,在內存中被清除。針對這種肯定不會出現循環引用的情況,蘋果可能為了簡化代碼,讓開發者無需顯式的寫出self,當然你要寫也是能正常編譯的。
2 逃逸閉包為什么需要寫self
當執行testEscapingClosure函數的時候,函數體創建了一個局部變量引用著callBack閉包,閉包中要訪問self中的成員變量需要引用self,但是要注意的是這里只能弱引用[weak self]
或者指向self所在內存(引用計數不增加)[unowned self]
,圖示用虛線體現出來了,這樣就避免了循環引用,而這個操作是需要開發人員自己通過代碼處理的[unowned self]
[weak self]
,針對這種可能會出現循環引用的情況,蘋果希望開發者顯式的寫出self。
需要注意的是下面的代碼也是逃逸閉包,為什么?我畫個圖能把這個問題說清楚
override func viewDidLoad() {
print("任務1")
DispatchQueue.main.async {
self.x = 1 // 逃逸閉包需要顯式的寫出self
}
print("任務3")
}
DispatchQueue.main.async的作用其實就是將閉包任務添加到Main隊列中(往指定隊列隊尾追加),同時指定在執行閉包的時候是否創建新線程(這里雖然是異步,但是在主隊列,所以不會創建新線程),之后再執行任務3,任務3執行完畢,函數viewDidLoad就結束了,如圖所示,閉包任務并沒有在viewDidLoad中,他不僅要等viewDidLoad結束,還得等viewWillAppear....結束才能開始執行,既然如此,當然就是逃逸閉包了。
自動閉包
自動閉包是一種自動創建的閉包,用于包裝傳遞給函數作為參數的表達式。這種閉包不接受任何參數,當它被調用的時候,會返回被包裝在其中的表達式的返回值。
下面看段自動閉包的代碼事例:
class ViewController: UIViewController {
let aa = { () -> String in
print("do some thing")
return "Autoclosures test"
}
func test(_ f: () -> String) {
print(f())
}
// 將傳進來的 表達式 自動閉包
func testAutoclosures(_ f: @autoclosure () -> String) {
print(f())
}
override func viewDidLoad() {
super.viewDidLoad()
// 直接傳閉包代碼塊
test { () -> String in
print("aaa");
return "hahaha"
}
// 傳閉包名
test(aa)
// 表達式 aa() 將會被自動閉包
testAutoclosures(aa())
}
}
上面這段代碼事例默認是非逃逸閉包,有時候可能有逃逸閉包的場景,下面舉個自動閉包又是逃逸閉包的例子:(注意看代碼上的注釋,我這里把解釋通過注釋的形式體現出來了)
class ViewController: UIViewController {
var x = 10
var closureArray: [() -> String] = []
func bb() -> String {
x = 20
return String(x)
}
func testAutoclosure(_ f: @escaping () -> String) {
closureArray.append(f)
}
// 自動閉包,并且是逃逸閉包
func testAutoclosure2(_ f: @autoclosure @escaping () -> String) {
closureArray.append(f)
}
override func viewDidLoad() {
super.viewDidLoad()
// 直接通過閉包名傳遞,注意:這種寫法會retainCycle
// testAutoclosure(bb)
// 既然上面會出現retainCycle,那么自己包裝一個閉包,在閉包中解決retainCycle
testAutoclosure { [unowned self] () -> String in
self.bb()
}
// 注意:這種寫法會retainCycle 這里不仔細是非常容易犯錯的
// testAutoclosure2(self.bb())
// @autoclosure會自動幫我們生成閉包
unowned let assingSelf = self
testAutoclosure2(assingSelf.bb()) // 參數傳 表達式 ,閉包處理蘋果會自動處理
// 用weak解決循環引用
weak var weakSelf = self
testAutoclosure2( (weakSelf?.bb())! )
}
}
關于上面這個例子,有細節部分需要特別注意的,很容易忽略導致內存泄露,testAutoclosure2(xxxx)
為什么要用weak引用或者unowned?我嘗試通過圖來說明一下:
當我們封裝完的一個函數,恰好有一個場景可以把函數轉成閉包直接用的時候,這時自動閉包就起作用了,代碼看起來會簡短很多,不過個人建議這種代碼還是少用為妙,因為他并不易讀。
循環引用
關于循環引用,上面分析逃逸閉包和自動閉包的時候也有做描述,如果還不是很明白為什么會出現循環引用,可以參考一下筆者的這篇文章,IOS之block和內存那些事,雖然用的是OC舉得例子,其實原理差不多的。然后我還想強調的是,不要死記會出現循環引用的場景和解決辦法,應該分析內存去理解,只有理解了,代碼千變萬化,自要保持冷靜去分析,相信都能思考出想要的答案。
閉包捕獲、閉包是引用類型
閉包可以在其被定義的上下文中捕獲常量或變量。即使定義這些常量和變量的原作用域已經不存在,閉包仍然可以在閉包函數體內引用和修改這些值。
class ViewController: UIViewController {
func testCapture() -> () -> Int {
var count = 0
func closureFunc() -> Int {
count = count + 10
return count
}
return closureFunc
}
override func viewDidLoad() {
super.viewDidLoad()
let test1 = testCapture()
print(test1()) // 10
print(test1()) // 20
print(test1()) // 30
let test2 = test1
print(test2()) // 40
print(test2()) // 50
let test3 = testCapture()
print(test3()) // 10
print(test1()) // 60
}
}
這段代碼看起來非常好理解,不過有個疑惑,看圖
test1和test2內存地址一樣好理解,因為是引用類型賦值,但是test3內存地址也一樣,如果test3內存地址一樣,那么最后一句print(test1())
為什么不是20?
類似的疑惑
class ViewController: UIViewController {
// 逃逸閉包
var x = 10
var callBackArray: [() -> Void] = []
func testEscapingClosure(callBack: @escaping () -> Void) {
callBackArray.append(callBack)
}
override func viewDidLoad() {
super.viewDidLoad()
testEscapingClosure { [unowned self] () -> Void in
self.x = 100 // self -> 0x000060000000c821
}
testEscapingClosure { [weak self] () -> Void in
self?.x = 100 // self -> 0x7ff940408310
}
// self -> 0x7ff940408310
callBackArray[0]()
callBackArray[1]()
}
}
從注釋可以看出captureList里的self,用weak修飾的self內存地址和外面的self內存地址一致,這也和預期想的一樣,然而unowned里的self內存地址變了,難道不也是和外面的self地址一樣,只不過是引用計數不加1?這個問題我正在研究,回頭再分享。