[0. Brief introduction of block]
Block是iOS4.0+ 和Mac OS X 10.6+ 引進的對C語言的擴充,用來實現匿名函數的特性。
用維基百科的話來說,Block是Apple Inc.為C、C++以及Objective-C添加的特性,使得這些語言可以用類lambda運算式的文法來建立閉包。
用Apple文檔的話來說,A block is an anonymous inline collection of code, and sometimes also called a "closure".
關於閉包,我覺得阮一峰的一句話解釋簡潔明了:閉包就是能夠讀取其它函數內部變數的函數。
這個解釋用到block來也很恰當:一個函數裡定義了個block,這個block可以訪問該函數的內部變數。
一個簡單的Block樣本如下:
[cpp]
view plaincopy
- int (^maxBlock)(int, int) = ^(int x, int y) { return x > y ? x : y; };
如果用Python的lambda運算式來寫,可以寫成如下形式:
[python]
view plaincopy
- f = lambda x, y : x if x > y else y
不過由於Python自身的語言特性,在def定義的函數體中,可以很自然地再用def語句定義內嵌函數,因為這些函數本質上都是對象。
如果用BNF來表示block的上下文無關文法,大致如下:
[cpp]
view plaincopy
- block_expression ::= ^ block_declare block_statement
- block_declare ::= block_return_type block_argument_list
- block_return_type ::= return_type | 空
- block_argument_list ::= argument_list | 空
[1. Why block]
Block除了能夠定義參數列表、傳回型別外,還能夠擷取被定義時的詞法範圍內的狀態(比如局部變數),並且在一定條件下(比如使用__block變數)能夠修改這些狀態。此外,這些可修改的狀態在相同詞法範圍內的多個block之間是共用的,即便出了該詞法範圍(比如棧展開,出了範圍),仍可以繼續共用或者修改這些狀態。
通常來說,block都是一些簡短程式碼片段的封裝,適用作工作單元,通常用來做並發任務、遍曆、以及回調。
比如我們可以在遍曆NSArray時做一些事情:
[cpp]
view plaincopy
- - (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
其中將stop設為YES,就跳出迴圈,不繼續遍曆了。
而在很多架構中,block越來越經常被用作回呼函數,取代傳統的回調方式。
- 用block作為回呼函數,可以使得程式員在寫代碼更順暢,不用中途跑到另一個地方寫一個回呼函數,有時還要考慮這個回呼函數放在哪裡比較合適。採用block,可以在調用函數時直接寫後續處理代碼,將其作為參數傳遞過去,供其任務執行結束時回調。
- 另一個好處,就是採用block作為回調,可以直接存取局部變數。比如我要在一批使用者中修改一個使用者的name,修改完成後通過回調更新對應使用者的儲存格UI。這時候我需要知道對應使用者儲存格的index,如果採用傳統回調方式,要嘛需要將index帶過去,回調時再回傳過來;要嘛通過外部範圍記錄當前操作儲存格的index(這限制了一次只能修改一個使用者的name);要嘛遍曆找到對應使用者。而使用block,則可以直接存取儲存格的index。
這份文檔中提到block的幾種適用場合:
- 任務完成時回調處理
- 訊息監聽回調處理
- 錯誤回調處理
- 枚舉回調
- 視圖動畫、變換
- 排序
[2. About __block_impl]
Clang提供了中間代碼展示的選項供我們進一步瞭解block的原理。
以一段很簡單的代碼為例:
使用-rewrite-objc選項編譯:
得到一份block0.cpp檔案,在這份檔案中可以看到如下程式碼片段:
從命名可以看出這是block的實現,並且得知block在Clang編譯器前端得到實現,可以產生C中間代碼。很多語言都可以只實現編譯器前端,產生C中間代碼,然後利用現有的很多C編譯器後端。
從結構體的成員可以看出,Flags、Reserved可以先略過,isa指標表明了block可以是一個NSObject,而FuncPtr指標顯然是block對應的函數指標。
由此,揭開了block的神秘面紗。
不過,block相關的變數放哪裡呢?上面提到block可以capture詞法範圍內(或者說是外層上下文、範圍)的狀態,即便是出了該範圍,仍然可以修改這些狀態。這是如何做到的呢?
[3. Implementation of a simple block]
先看一個只輸出一句話的block是怎麼樣的。
產生中間代碼,得到片段如下:
首先出現的結構體就是__main_block_impl_0,可以看出是根據所在函數(main函數)以及出現序列(第0個)進行命名的。如果是全域block,就根據變數名和出現序列進行命名。__main_block_impl_0中包含了兩個成員變數和一個建構函式,成員變數分別是__block_impl結構體和描述資訊Desc,之後在建構函式中初始化block的類型資訊和函數指標等資訊。
接著出現的是__main_block_func_0函數,即block對應的函數體。該函數接受一個__cself參數,即對應的block自身。
再下面是__main_block_desc_0結構體,其中比較有價值的資訊是block大小。
最後就是main函數中對block的建立和調用,可以看出執行block就是調用一個以block自身作為參數的函數,這個函數對應著block的執行體。
這裡,block的類型用_NSConcreteStackBlock來表示,表明這個block位於棧中。同樣地,還有_NSConcreteMallocBlock和_NSConcreteGlobalBlock。
由於block也是NSObject,我們可以對其進行retain操作。不過在將block作為回呼函數傳遞給底層架構時,底層架構需要對其copy一份。比方說,如果將回調block作為屬性,不能用retain,而要用copy。我們通常會將block寫在棧中,而需要回調時,往往回調block已經不在棧中了,使用copy屬性可以將block放到堆中。或者使用Block_copy()和Block_release()。
[4. Capture local variable]
再看一個訪問局部變數的block是怎樣的。
產生中間代碼,得到片段如下:
可以看出這次的block結構體__main_block_impl_0多了個成員變數i,用來儲存使用到的局部變數i(值為1024);並且此時可以看到__cself參數的作用,類似C++中的this和Objective-C的self。
如果我們嘗試修改局部變數i,則會得到如下錯誤:
錯誤資訊很詳細,既告訴我們變數不可賦值,也提醒我們要使用__block類型標識符。
為什麼不能給變數i賦值呢?
因為main函數中的局部變數i和函數__main_block_func_0不在同一個範圍中,調用過程中只是進行了值傳遞。當然,在上面代碼中,我們可以通過指標來實現局部變數的修改。不過這是由於在調用__main_block_func_0時,main函數棧還沒展開完成,變數i還在棧中。但是在很多情況下,block是作為參數傳遞以供後續回調執行的。通常在這些情況下,block被執行時,定義時所在的函數棧已經被展開,局部變數已經不在棧中了(block此時在哪裡?),再用指標訪問就⋯⋯。
所以,對於auto類型的局部變數,不允許block進行修改是合理的。
[5. Modify static local variable]
於是我們也可以推斷出,靜態局部變數是如何在block執行體中被修改的——通過指標。
因為靜態局部變數存在於資料區段中,不存在棧展開後非法訪存的風險。
上面中間程式碼片段與前一個片段的差別主要在於main函數裡傳遞的是i的地址(&i),以及__main_block_impl_0結構體中成員i變成指標類型(int
*)。
然後在執行block時,通過指標修改值。
當然,全域變數、靜態全域變數都可以在block執行體內被修改。更準確地講,block可以修改它被調用(這裡是__main_block_func_0)時所處範圍內的變數。比如一個block作為成員變數時,它也可以訪問同一個對象裡的其它成員變數。
[6. Implementation of __block variable]
那麼,__block類型變數是如何支援修改的呢?
我們為int類型變數加上__block指示符,使得變數i可以在block函數體中被修改。
此時再看中間代碼,會多出很多資訊。首先是__block變數對應的結構體:
由第一個成員__isa指標也可以知道__Block_byref_i_0也可以是NSObject。
第二個成員__forwarding指向自己,為什麼要指向自己?指向自己是沒有意義的,只能說有時候需要指向另一個__Block_byref_i_0結構。
最後一個成員是目標儲存變數i。
此時,__main_block_impl_0結構如下:
__main_block_impl_0的成員變數i變成了__Block_byref_i_0 *類型。
對應的函數__main_block_func_0如下:
亮點是__Block_byref_i_0指標類型變數i,通過其成員變數__forwarding指標來操作另一個成員變數。
而main函數如下:
通過這樣看起來有點複雜的改變,我們可以修改變數i的值。但是問題同樣存在:__Block_byref_i_0類型變數i仍然處於棧上,當block被回調執行時,變數i所在的棧已經被展開,怎麼辦?
在這種關鍵時刻,__main_block_desc_0站出來了:
此時,__main_block_desc_0多了兩個成員函數:copy和dispose,分別指向__main_block_copy_0和__main_block_dispose_0。
當block從棧上被copy到堆上時,會調用__main_block_copy_0將__block類型的成員變數i從棧上複製到堆上;而當block被釋放時,相應地會調用__main_block_dispose_0來釋放__block類型的成員變數i。
一會在棧上,一會在堆上,那如果棧上和堆上同時對該變數進行操作,怎麼辦?
這時候,__forwarding的作用就體現出來了:當一個__block變數從棧上被複製到堆上時,棧上的那個__Block_byref_i_0結構體中的__forwarding指標也會指向堆上的結構。
/* ---------------------------------------------------------------------------------------------------- */
本來還想繼續寫下去,結果發現文章有點長了。先到此。
原文連結:http://blog.csdn.net/jasonblog/article/details/7756763
Jason Lee @ Hangzhou