標籤:
[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示比例如以下:
int (^maxBlock)(int, int) = ^(int x, int y) { return x > y ? x : y; };
假設用Python的lambda運算式來寫,能夠寫成例如以下形式:
f = lambda x, y : x if x > y else y
只是由於Python自身的語言特性,在def定義的函數體中,能夠非常自然地再用def語句定義內嵌函數,由於這些函數本質上都是對象。
假設用BNF來表示block的上下文無關文法,大致例如以下:
block_expression ::= ^ block_declare block_statementblock_declare ::= block_return_type block_argument_listblock_return_type ::= return_type | 空block_argument_list ::= argument_list | 空
[1. Why block]
Block除了可以定義參數列表、傳回型別外,還可以擷取被定義時的詞法範圍內的狀態(比方局部變數),而且在一定條件下(比方使用__block變數)可以改動這些狀態。此外,這些可改動的狀態在同樣詞法範圍內的多個block之間是共用的,即便出了該詞法範圍(比方棧展開,出了範圍),仍可以繼續共用或者改動這些狀態。
通常來說,block都是一些簡短程式碼片段的封裝,適用作工作單元,通經常使用來做並發任務、遍曆、以及回調。
比方我們能夠在遍曆NSArray時做一些事情:
- (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的原理。
以一段非常easy的代碼為例:
使用-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
iOS中block實現的探究