Swift進階09:逃逸閉包 & 自動閉包

逃逸閉包 & 非逃逸閉包

逃逸閉包定義

閉包作為一個實際參數(shù)傳遞給一個函數(shù)時,并且是在函數(shù)返回之后調(diào)用,我們就說這個閉包逃逸了。當聲明一個接受閉包作為形式參數(shù)的函數(shù)時,可以在形式參數(shù)前寫@escaping明確閉包是允許逃逸的

  • 如果用@escaping修飾閉包后,我們必須顯示的在閉包中使用self
  • swift3.0之后,系統(tǒng)默認閉包參數(shù)就是被@noescape,可以通過SIL來驗證
image

image

非逃逸閉包的特點:

  • 1、執(zhí)行時機:在函數(shù)體內(nèi)執(zhí)行
  • 2、閉包生命周期:函數(shù)執(zhí)行完之后,閉包也就消失了

逃逸閉包的兩種調(diào)用情況

  • 1、作為屬性存儲,在后面進行調(diào)用
  • 2、延遲調(diào)用

1、作為屬性

閉包作為存儲屬性時,主要有以下幾點說明:

  • 1、定義一個閉包屬性
  • 2、在方法中對閉包屬性進行賦值
  • 3、在合適的時機調(diào)用(與業(yè)務邏輯相關)

如下所示,當前的complitionHandler作為HTTeacher的屬性,是在方法makeIncrementer調(diào)用完成后才會調(diào)用,這時,閉包的生命周期要比當前方法的生命周期長

//*********1、閉包作為屬性
class HTTeacher {
    //定義一個閉包屬性
    var complitionHandler: ((Int)->Void)?
    //函數(shù)參數(shù)使用@escaping修飾,表示允許函數(shù)返回之后調(diào)用
    func makeIncrementer(amount: Int, handler: @escaping (Int)->Void){
        var runningTotal = 0
        runningTotal += amount
        //賦值給屬性
        complitionHandler = handler
    }

    func doSomething(){
        self.makeIncrementer(amount: 10) {
            print($0)
        }
    }

    deinit {
        print("HTTeacher deinit")
    }
}
//使用
var t = HTTeacher()
t.doSomething()
t.complitionHandler?(20)

//<!--打印結(jié)果-->
//20

2、延遲調(diào)用

  • 【延遲方法中使用】 1、在延遲方法中調(diào)用逃逸閉包
class HTTeacher {
    //定義一個閉包屬性
    var complitionHandler: ((Int)->Void)?
    
    let serialQueue = DispatchQueue(label: "networkRequests")

    
    //函數(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("HTTeacher deinit")
    }
}

//使用
var t = HTTeacher()
t.doSomething()

//---打印結(jié)果
函數(shù)執(zhí)行完了
HTTeacher deinit
逃逸閉包延遲執(zhí)行
10

當前方法執(zhí)行的過程中不會等待閉包執(zhí)行完成后再執(zhí)行,而是直接返回,所以當前閉包的生命周期要比方法長

逃逸閉包 vs 非逃逸閉包 區(qū)別

  • 非逃逸閉包:一個接受閉包作為參數(shù)的函數(shù),閉包是在這個函數(shù)結(jié)束前內(nèi)被調(diào)用,即可以理解為閉包是在函數(shù)作用域結(jié)束前被調(diào)用
    • 1、不會產(chǎn)生循環(huán)引用,因為閉包的作用域在函數(shù)作用域內(nèi),在函數(shù)執(zhí)行完成后,就會釋放閉包捕獲的所有對象
    • 2、針對非逃逸閉包,編譯器會做優(yōu)化:省略內(nèi)存管理調(diào)用
    • 3、非逃逸閉包捕獲的上下文保存在棧上,而不是堆上(官方文檔說明)。
  • 逃逸閉包:一個接受閉包作為參數(shù)的函數(shù),逃逸閉包可能會在函數(shù)返回之后才被調(diào)用,即閉包逃離了函數(shù)的作用域
    • 1、可能會產(chǎn)生循環(huán)引用,因為逃逸閉包中需要顯式的引用self(猜測其原因是為了提醒開發(fā)者,這里可能會出現(xiàn)循環(huán)引用了),而self可能是持有閉包變量的(與OC中block的的循環(huán)引用類似)
    • 2、一般用于異步函數(shù)的返回,例如網(wǎng)絡請求
  • 使用建議:如果沒有特別需要,開發(fā)中使用非逃逸閉包是有利于內(nèi)存優(yōu)化的,所以蘋果把閉包區(qū)分為兩種,特殊情況時再使用逃逸閉包

自動閉包

有下面一個例子,當conditiontrue時,會打印錯誤信息,即如果是false,當前條件不會執(zhí)行

//1、condition為false時,當前條件不會執(zhí)行
func debugOutPrint(_ condition: Bool, _ message: String){
    if condition {
        print("debug: \(message)")
    }
}
debugOutPrint(true, "Application Error Occured")
  • 如果字符串是在某個業(yè)務邏輯中獲取的,會出現(xiàn)什么情況?
func debugOutPrint(_ condition: Bool, _ message: String){
    if condition {
        print("debug: \(message)")
    }
}

func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}

//---如果傳入true
debugOutPrint(true, doSomething())
//----打印結(jié)果
//doSomething
//debug: Network Error Occured

//---如果傳入false
debugOutPrint(false, doSomething())
//---打印結(jié)果
//doSomething

通過結(jié)果發(fā)現(xiàn),無論是傳入true還是false,當前的方法都會執(zhí)行,如果這個方法是一個非常耗時的操作,這里就會造成一定的資源浪費。所以為了避免這種情況,需要將當前參數(shù)修改為一個閉包

//3、為了避免資源浪費,將當前參數(shù)修改成一個閉包
func debugOutPrint(_ condition: Bool, _ message: () -> String){
    if condition {
        print("debug: \(message())")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}
debugOutPrint(false, doSomething)

修改后,當傳入為false時,doSomething函數(shù)不會執(zhí)行

  • 如果此時傳入一個string,又需要如何處理呢?

可以通過@autoclosure將當前的閉包聲明成一個自動閉包不接收任何參數(shù),返回值是當前內(nèi)部表達式的值。所以當傳入一個String時,其實就是將String放入一個閉包表達式中,在調(diào)用的時候返回

//4、將當前參數(shù)修改成一個閉包,并使用@autoclosure聲明成一個自動閉包
func debugOutPrint(_ condition: Bool, _ message: @autoclosure () -> String){
    if condition {
        print("debug: \(message())")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}
//---使用1:傳入函數(shù)
debugOutPrint(true, doSomething())

//---使用2:傳入字符串
debugOutPrint(true, "Application Error Occured")
image

自動閉包就相當于

debugOutPrint(true, "Application Error Occured")

相當于用{}包裹傳入的對象,然后返回{}內(nèi)的值
{
    //表達式里的值
    return "Network Error Occured"
}

總結(jié)

  • 逃逸閉包:一個接受閉包作為參數(shù)的函數(shù),逃逸閉包可能會在函數(shù)返回之后才被調(diào)用,即閉包逃離了函數(shù)的作用域,例如網(wǎng)絡請求,需要在形參前面使用@escaping來明確閉包是允許逃逸的。

    • 一般用于異步函數(shù)的回調(diào),比如網(wǎng)絡請求
    • 如果標記為了@escaping,必須在閉包中顯式的引用self
  • 非逃逸閉包:一個接受閉包作為參數(shù)的函數(shù),閉包是在這個函數(shù)結(jié)束前內(nèi)被調(diào)用,即可以理解為閉包是在函數(shù)作用域結(jié)束前被調(diào)用

  • 為什么要區(qū)分@escaping@nonescaping

    • 1、為了內(nèi)存管理,閉包會強引用它捕獲的所有對象,這樣閉包會持有當前對象,容易導致循環(huán)引用
    • 2、非逃逸閉包不會產(chǎn)生循環(huán)引用,它會在函數(shù)作用域內(nèi)使用,編譯器可以保證在函數(shù)結(jié)束時閉包會釋放它捕獲的所有對象
    • 3、使用非逃逸閉包可以使編譯器應用更多強有力的性能優(yōu)化,例如,當明確了一個閉包的生命周期的話,就可以省去一些保留(retain)和釋放(release)的調(diào)用
    • 4、非逃逸閉包它的上下文的內(nèi)存可以保存在棧上而不是堆上
  • 總結(jié):如果沒有特別需要,開發(fā)中使用非逃逸閉包是有利于內(nèi)存優(yōu)化的,所以蘋果把閉包區(qū)分為兩種,特殊情況時再使用逃逸閉包

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容