讓我們來深入淺出block吧

kuailejim發表於2016-12-13

開始之前,我想先提幾個問題,看看大家是否對此有疑惑。唐巧已經寫過一篇對block很有研究的文章,大家可以去看看(本文會部分引用巧哥文中出現的圖和程式碼)。在巧哥的基礎上,我補充一些block相關的知識點和程式碼,並且概括並修正一些觀點。

1.block是什麼?block是物件嗎?

2.block分為哪幾種?__blcok關鍵字的作用?

3.block在ARC和MRC下的區別?

4.block的生命週期?

5.block對於以引數形式傳進來的物件,會不會強引用??


block是什麼?block是物件嗎?

先介紹一下什麼是閉包。在 wikipedia 上,閉包的定義) 是:

In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.

翻譯過來,閉包是一個函式(或指向函式的指標),再加上該函式執行的外部的上下文變數(有時候也稱作自由變數)。

block 實際上就是 Objective-C 語言對於閉包的實現。

block是不是物件?答案顯而易見:是的。

下圖是block的資料結構定義,顯而易見,在Block_layout裡,我們看到了isa指標,這裡我們不具體對isa指標展開,也不對block具體資料結構展開,想了解詳細可以看唐巧的文章。

回到上文,為什麼說block是物件呢,原因就在於isa指標。那麼這個isa指標是何物呢?

所有物件的都有isa 指標,用於實現物件相關的功能。

看到這,你應該明白,block其實就是objc對於閉包的物件實現。

讓我們來深入淺出block吧


block分為哪幾種?__blcok關鍵字的作用?

分為三種,即NSConcreteGlobalBlock、NSConcreteStackBlock、NSConcreteMallocBlock。

詳細剖析這三種block,首先是NSConcreteGlobalBlock:

簡單地講,如果一個block鐘沒有引用外部變數並且沒有被其他物件持有,就是NSConcreteGlobalBlock。

如下圖所示:

讓我們來深入淺出block吧

需要注意的是,NSConcreteGlobalBlock是全域性的block,在編譯期間就已經決定了,如同巨集一樣。

什麼是NSConcreteStackBlock呢:

可以這麼理解,NSConcreteStackBlock就是引用了外部變數的block,上程式碼:

讓我們來深入淺出block吧

OK,我們已經知道了NSConcreteStackBlock,那麼它和NSConcreteGlobalBlock有什麼區別呢?難道僅僅是引用了外部變數與否的區別嗎?答案是否定的。

其實NSConcreteStackBlock內部會有一個結構體__main_block_impl_0,這個結構體會儲存外部變數,使其體積變大。而這就導致了NSConcreteStackBlock並不像巨集一樣,而是一個動態的物件。而它由於沒有被持有,所以在它的內部,它也不會持有其外部引用的物件。

證據如下:

讓我們來深入淺出block吧

從列印的日誌可以看出,引用計數始終沒變。

NSConcreteMallocBlock:

看似最為神祕的NSConcreteMallocBlock其實就是一個block被copy時,將生成NSConcreteMallocBlock(block沒有retain)。怎麼樣,是不是很簡單0 0

讓我們來深入淺出block吧

需要注意的是,NSConcreteMallocBlock會持有外部物件!

讓我們來深入淺出block吧

看到了吧,只要這個NSConcreteMallocBlock存在,內部物件的引用計數就會+1。

下面來說說__block這個關鍵字:

先上一個例子,你們很快就會明白了

讓我們來深入淺出block吧

沒錯,前文說過,block引用外部是以捕獲的形式來捕捉的,而沒有宣告block,則會將外部變數copy進block,若用了block,則是複製其引用地址來實現訪問。這就是為什麼宣告瞭__block,在block內部改變就會對外有影響的原因了。

注意!!這裡需要知道的是,在MRC環境下,如果沒有用block,會對外部物件採用copy的操作,而用了block則不會用copy的操作。

上程式碼:

讓我們來深入淺出block吧

哈哈哈,怎麼樣,所以從更底層的角度來說,在MRC環境下,__block根本不會對指標所指向的物件執行copy操作,而只是把指標進行的複製。而這一點往往是很多新手&老手所不知道的!

而在ARC環境下,對於宣告為__block的外部物件,在block內部會進行retain,以至於在block環境內能安全的引用外部物件,所以要謹防迴圈引用的問題!


block在ARC和MRC下的區別?

首先要指正下巧哥部落格的觀點:

在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 型別的 block。

在上面介紹NSConcreteStackBlock的時候,是在ARC環境下跑的,而列印出來的日誌明確的顯示出,當時的block型別為NSConcreteStackBlock。

而實際上,為什麼大家普遍會認為ARC下不存在NSConcreteStackBlock呢?

這是因為本身我們常常將block賦值給變數,而ARC下預設的賦值操作是strong的,到了block身上自然就成了copy,所以常常列印出來的block就是NSConcreteMallocBlock了。

so,在ARC下,大部分的應用場景下,幾乎可以說是全部都為NSConcreteMallocBlock或者是NSConcreteGlobalBlock。那麼問題來了,我們知道NSConcreteMallocBlock是會持有外部變數的,而此時如果它所持有的外部變數正好又持有它,就會產生迴圈引用的問題。

讓我們來聊聊block的生命週期!


block的生命週期?

談到block生命週期,其實這是一個非常嚴肅的話題,雖然block簡單易用,老少皆宜,但是一旦使用不慎容易造成“強擼灰飛煙滅”的後果(記憶體洩露)。

ps:接下來的例子都用ARC來展示了

首先展示:

讓我們來深入淺出block吧

不用看了,這個object永遠也不會被釋放,這是一個很典型的迴圈引用情形。object持有了block(讀者可以想象此處為何為NSConcreteMallocBlock,提示:在ARC環境下),而block又持有了object,於是造成死鎖,object再也不會被釋放了。此時機智的編譯器給了你warning,但是在很多複雜的情況下,編譯器並不能識別出迴圈引用的場景。而此時你就需要注意了!

那麼,我是如何來處理block的生命週期相關問題的呢,首先前文提到,block是一個物件,既然是一個物件,它必然有著和物件一樣的生命週期即如果沒有被引用就會被釋放。

所以block的生命週期歸結起來很簡單,只要看持有block的物件是不是也被block持有,如果沒有持有,就不用擔心迴圈引用問題了。

但是像上面的情況,如果產生相互持有的情況該腫麼辦!

你可以用weak(ARC)或block(MRC)來解決:

讓我們來深入淺出block吧

看,現在就可以愉快的釋放了。


block對於以引數形式傳進來的物件,會不會強引用?

唉,不知不覺已經快半夜2點了,對於這部分的話,其實也是閒著蛋疼在想這個問題。

其實block與函式和方法一樣,對於傳進來的引數,並不會持有

證據如下:

讓我們來深入淺出block吧


總結:

到這裡,對於block的介紹結束了。實際運用中其實不用太關心這些原理的,只需要正確掌握好block的生命週期就可以靈活地運用block了。但是對於一個資深開發者來說,block的深層次掌握還是必須的!

好的,下次再見!

歡迎大家關注@kuailejim,我會經常在上面分享一些大家感興趣的東西。

相關文章