Swift閉包(二):進階篇

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?這個問題我正在研究,回頭再分享。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容