Swift-進階 09:閉包(二)逃逸閉包 & 非逃逸閉包

Swift 進階之路 文章匯總

本文主要分析逃逸閉包 、非逃逸閉包、自動閉包

逃逸閉包 & 非逃逸閉包

逃逸閉包定義

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

  • 如果用@escaping修飾閉包后,我們必須顯示的在閉包中使用self

  • swift3.0之后岖圈,系統(tǒng)默認閉包參數(shù)就是被@nonescaping讹语,可以通過SIL來驗證

    SIL驗證

    • 1、執(zhí)行時機:在函數(shù)體內(nèi)執(zhí)行

    • 2幅狮、閉包生命周期:函數(shù)執(zhí)行完之后募强,閉包也就消失了

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

  • 1株灸、延遲調(diào)用

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

1慌烧、作為屬性

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

  • 1、定義一個閉包屬性

  • 2屹蚊、在方法中對閉包屬性進行賦值

  • 3厕氨、在合適的時機調(diào)用(與業(yè)務(wù)邏輯相關(guān))

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

//*********1嘱兼、閉包作為屬性
class CJLTeacher {
    //定義一個閉包屬性
    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
    }

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

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

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

2、延遲調(diào)用

  • 【延遲方法中使用】 1芹壕、在延遲方法中調(diào)用逃逸閉包
class CJLTeacher {
    //定義一個閉包屬性
    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("CJLTeacher deinit")
    }
}
//使用
var t = CJLTeacher()
t.doSomething()

<!--打印結(jié)果-->
函數(shù)執(zhí)行完了
逃逸閉包延遲執(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)絡(luò)請求

  • 使用建議:如果沒有特別需要,開發(fā)中使用非逃逸閉包是有利于內(nèi)存優(yōu)化的况褪,所以蘋果把閉包區(qū)分為兩種撕贞,特殊情況時再使用逃逸閉包

自動閉包

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

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

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

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

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

  • 【修改】:將message參數(shù)修改成一個閉包地消,需要傳入的是一個函數(shù)
//3、為了避免資源浪費畏妖,將當前參數(shù)修改成一個閉包
func debugOutPrint(_ condition: Bool, _ message: () -> String){
    if condition {
        print("cjl_debug: \(message())")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}
debugOutPrint(true, doSomething)

修改后運行結(jié)果如下


運行結(jié)果
  • 如果此時傳入一個string脉执,又需要如何處理呢?
    可以通過@autoclosure將當前的閉包聲明成一個自動閉包戒劫,不接收任何參數(shù)半夷,返回值是當前內(nèi)部表達式的值。所以當傳入一個String時迅细,其實就是將String放入一個閉包表達式中巫橄,在調(diào)用的時候返回
//4、將當前參數(shù)修改成一個閉包茵典,并使用@autoclosure聲明成一個自動閉包
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")

<!--打印結(jié)果-->
doSomething
cjl_debug: Network Error Occured

cjl_debug: Application Error Occured

自動閉包就相當于

debugOutPrint(true, "Application Error Occured")

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

總結(jié)

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

    • 一般用于異步函數(shù)的回調(diào)帆离,比如網(wǎng)絡(luò)請求

    • 如果標記為了@escaping蔬蕊,必須在閉包中顯式的引用self

  • 非逃逸閉包:一個接受閉包作為參數(shù)的函數(shù),閉包是在這個函數(shù)結(jié)束前內(nèi)被調(diào)用哥谷,即可以理解為閉包是在函數(shù)作用域結(jié)束前被調(diào)用

  • 為什么要區(qū)分@escaping 和 @nonescaping岸夯?

    • 1、為了內(nèi)存管理们妥,閉包會強引用它捕獲的所有對象猜扮,這樣閉包會持有當前對象,容易導(dǎo)致循環(huán)引用

    • 2王悍、非逃逸閉包不會產(chǎn)生循環(huán)引用破镰,它會在函數(shù)作用域內(nèi)使用餐曼,編譯器可以保證在函數(shù)結(jié)束時閉包會釋放它捕獲的所有對象

    • 3压储、使用非逃逸閉包可以使編譯器應(yīng)用更多強有力的性能優(yōu)化,例如源譬,當明確了一個閉包的生命周期的話集惋,就可以省去一些保留(retain)和釋放(release)的調(diào)用

    • 4、非逃逸閉包它的上下文的內(nèi)存可以保存在棧上而不是堆上

  • 總結(jié):如果沒有特別需要踩娘,開發(fā)中使用非逃逸閉包是有利于內(nèi)存優(yōu)化的刮刑,所以蘋果把閉包區(qū)分為兩種,特殊情況時再使用逃逸閉包

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末养渴,一起剝皮案震驚了整個濱河市雷绢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌理卑,老刑警劉巖翘紊,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異藐唠,居然都是意外死亡帆疟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門宇立,熙熙樓的掌柜王于貴愁眉苦臉地迎上來踪宠,“玉大人,你說我怎么就攤上這事妈嘹×粒” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長柬脸。 經(jīng)常有香客問我痘绎,道長,這世上最難降的妖魔是什么肖粮? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任孤页,我火速辦了婚禮,結(jié)果婚禮上涩馆,老公的妹妹穿的比我還像新娘行施。我一直安慰自己,他們只是感情好魂那,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布蛾号。 她就那樣靜靜地躺著,像睡著了一般涯雅。 火紅的嫁衣襯著肌膚如雪鲜结。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天活逆,我揣著相機與錄音精刷,去河邊找鬼。 笑死蔗候,一個胖子當著我的面吹牛怒允,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播锈遥,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纫事,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了所灸?” 一聲冷哼從身側(cè)響起丽惶,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎爬立,沒想到半個月后钾唬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡懦尝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年知纷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陵霉。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡琅轧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出踊挠,到底是詐尸還是另有隱情乍桂,我是刑警寧澤冲杀,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站睹酌,受9級特大地震影響权谁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜憋沿,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一旺芽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辐啄,春花似錦采章、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至砸民,卻和暖如春抵怎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背岭参。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工反惕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冗荸。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓承璃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蚌本。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容