iOS中block實現的探究

[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示例如下:

  1. int (^maxBlock)(intint) = ^(int x, int y) { return x > y ? x : y; };  

如果用Python的lambda表達式來寫,可以寫成如下形式:

[python] view plaincopy
  1. f = lambda x, y : x if x > y else y  

不過由於Python自身的語言特性,在def定義的函數體中,可以很自然地再用def語句定義內嵌函數,因爲這些函數本質上都是對象。

如果用BNF來表示block的上下文無關文法,大致如下:

  1. block_expression  ::=  ^  block_declare  block_statement  
  2. block_declare  ::=  block_return_type  block_argument_list  
  3. block_return_type ::=  return_type  |  空  
  4. block_argument_list  ::=  argument_list  |  空  


[1. Why block]

Block除了能夠定義參數列表、返回類型外,還能夠獲取被定義時的詞法範圍內的狀態(比如局部變量),並且在一定條件下(比如使用__block變量)能夠修改這些狀態。此外,這些可修改的狀態在相同詞法範圍內的多個block之間是共享的,即便出了該詞法範圍(比如棧展開,出了作用域),仍可以繼續共享或者修改這些狀態。

通常來說,block都是一些簡短代碼片段的封裝,適用作工作單元,通常用來做併發任務、遍歷、以及回調。

比如我們可以在遍歷NSArray時做一些事情:

  1. - (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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章