iOS中block的探究文/CocoaChina社區(qū)會員casual0402 /* ---------------------------------------------------------------------------------------------------- */ [0. Brief introduction of block] Block是iOS4.0+ 和Mac OS X 10.6+ 引進(jìn)的對C語言的擴(kuò)展,,用來實(shí)現(xiàn)匿名函數(shù)的特性。 用維基百科的話來說,,Block是Apple Inc.為C,、C++以及Objective-C添加的特性,使得這些語言可以用類lambda表達(dá)式的語法來創(chuàng)建閉包,。 用Apple文檔的話來說,,A block is an anonymous inline collection of code, and sometimes also called a "closure". 關(guān)于閉包,我覺得阮一峰的一句話解釋簡潔明了:閉包就是能夠讀取其它函數(shù)內(nèi)部變量的函數(shù),。 這個解釋用到block來也很恰當(dāng):一個函數(shù)里定義了個block,這個block可以訪問該函數(shù)的內(nèi)部變量,。 一個簡單的Block示例如下: int (^maxBlock)(int, int) = ^(int x, int y) { return x > y ? x : y; }; 如果用Python的lambda表達(dá)式來寫,,可以寫成如下形式: f = lambda x, y : x if x > y else y 不過由于Python自身的語言特性,在def定義的函數(shù)體中,,可以很自然地再用def語句定義內(nèi)嵌函數(shù),,因?yàn)檫@些函數(shù)本質(zhì)上都是對象。 如果用BNF來表示block的上下文無關(guān)文法,,大致如下: 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除了能夠定義參數(shù)列表,、返回類型外,還能夠獲取被定義時(shí)的詞法范圍內(nèi)的狀態(tài)(比如局部變量),,并且在一定條件下(比如使用__block變量)能夠修改這些狀態(tài),。此外,這些可修改的狀態(tài)在相同詞法范圍內(nèi)的多個block之間是共享的,,即便出了該詞法范圍(比如棧展開,,出了作用域),,仍可以繼續(xù)共享或者修改這些狀態(tài)。 通常來說,,block都是一些簡短代碼片段的封裝,,適用作工作單元,通常用來做并發(fā)任務(wù),、遍歷,、以及回調(diào)。 比如我們可以在遍歷NSArray時(shí)做一些事情: - (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block; 其中將stop設(shè)為YES,,就跳出循環(huán),,不繼續(xù)遍歷了。 而在很多框架中,,block越來越經(jīng)常被用作回調(diào)函數(shù),,取代傳統(tǒng)的回調(diào)方式。 用block作為回調(diào)函數(shù),,可以使得程序員在寫代碼更順暢,,不用中途跑到另一個地方寫一個回調(diào)函數(shù),有時(shí)還要考慮這個回調(diào)函數(shù)放在哪里比較合適,。采用block,,可以在調(diào)用函數(shù)時(shí)直接寫后續(xù)處理代碼,將其作為參數(shù)傳遞過去,,供其任務(wù)執(zhí)行結(jié)束時(shí)回調(diào),。 另一個好處,就是采用block作為回調(diào),,可以直接訪問局部變量,。比如我要在一批用戶中修改一個用戶的name,修改完成后通過回調(diào)更新對應(yīng)用戶的單元格UI,。這時(shí)候我需要知道對應(yīng)用戶單元格的index,,如果采用傳統(tǒng)回調(diào)方式,要嘛需要將index帶過去,,回調(diào)時(shí)再回傳過來,;要嘛通過外部作用域記錄當(dāng)前操作單元格的index(這限制了一次只能修改一個用戶的name);要嘛遍歷找到對應(yīng)用戶,。而使用block,,則可以直接訪問單元格的index。 這份文檔中提到block的幾種適用場合: 任務(wù)完成時(shí)回調(diào)
/* ---------------------------------------------------------------------------------------------------- */ [2. About __block_impl] Clang提供了中間代碼展示的選項(xiàng)供我們進(jìn)一步了解block的原理,。 以一段很簡單的代碼為例: 使用-rewrite-objc選項(xiàng)編譯: 得到一份block0.cpp文件,在這份文件中可以看到如下代碼片段: 從命名可以看出這是block的實(shí)現(xiàn),,并且得知block在Clang編譯器前端得到實(shí)現(xiàn),,可以生成C中間代碼,。很多語言都可以只實(shí)現(xiàn)編譯器前端,生成C中間代碼,,然后利用現(xiàn)有的很多C編譯器后端,。 從結(jié)構(gòu)體的成員可以看出,F(xiàn)lags,、Reserved可以先略過,,isa指針表明了block可以是一個NSObject,而FuncPtr指針顯然是block對應(yīng)的函數(shù)指針,。 由此,,揭開了block的神秘面紗。 不過,,block相關(guān)的變量放哪里呢,?上面提到block可以capture詞法范圍內(nèi)(或者說是外層上下文、作用域)的狀態(tài),,即便是出了該范圍,,仍然可以修改這些狀態(tài)。這是如何做到的呢,?
/* ---------------------------------------------------------------------------------------------------- */ [3. Implementation of a simple block] 先看一個只輸出一句話的block是怎么樣的,。 生成中間代碼,得到片段如下: 首先出現(xiàn)的結(jié)構(gòu)體就是__main_block_impl_0,,可以看出是根據(jù)所在函數(shù)(main函數(shù))以及出現(xiàn)序列(第0個)進(jìn)行命名的,。如果是全局block,就根據(jù)變量名和出現(xiàn)序列進(jìn)行命名,。__main_block_impl_0中包含了兩個成員變量和一個構(gòu)造函數(shù),,成員變量分別是__block_impl結(jié)構(gòu)體和描述信息Desc,之后在構(gòu)造函數(shù)中初始化block的類型信息和函數(shù)指針等信息,。 接著出現(xiàn)的是__main_block_func_0函數(shù),,即block對應(yīng)的函數(shù)體。該函數(shù)接受一個__cself參數(shù),,即對應(yīng)的block自身,。 再下面是__main_block_desc_0結(jié)構(gòu)體,,其中比較有價(jià)值的信息是block大小,。 最后就是main函數(shù)中對block的創(chuàng)建和調(diào)用,可以看出執(zhí)行block就是調(diào)用一個以block自身作為參數(shù)的函數(shù),,這個函數(shù)對應(yīng)著block的執(zhí)行體,。 這里,block的類型用_NSConcreteStackBlock來表示,,表明這個block位于棧中,。同樣地,,還有_NSConcreteMallocBlock和_NSConcreteGlobalBlock。 由于block也是NSObject,,我們可以對其進(jìn)行retain操作,。不過在將block作為回調(diào)函數(shù)傳遞給底層框架時(shí),底層框架需要對其copy一份,。比方說,,如果將回調(diào)block作為屬性,不能用retain,,而要用copy,。我們通常會將block寫在棧中,而需要回調(diào)時(shí),,往往回調(diào)block已經(jīng)不在棧中了,,使用copy屬性可以將block放到堆中?;蛘呤褂肂lock_copy()和Block_release(),。
/* ---------------------------------------------------------------------------------------------------- */ [4. Capture local variable] 再看一個訪問局部變量的block是怎樣的。 生成中間代碼,,得到片段如下: 可以看出這次的block結(jié)構(gòu)體__main_block_impl_0多了個成員變量i,,用來存儲使用到的局部變量i(值為1024);并且此時(shí)可以看到__cself參數(shù)的作用,,類似C++中的this和Objective-C的self,。 如果我們嘗試修改局部變量i,則會得到如下錯誤: 錯誤信息很詳細(xì),,既告訴我們變量不可賦值,,也提醒我們要使用__block類型標(biāo)識符。 為什么不能給變量i賦值呢,? 因?yàn)閙ain函數(shù)中的局部變量i和函數(shù)__main_block_func_0不在同一個作用域中,,調(diào)用過程中只是進(jìn)行了值傳遞。當(dāng)然,,在上面代碼中,,我們可以通過指針來實(shí)現(xiàn)局部變量的修改。不過這是由于在調(diào)用__main_block_func_0時(shí),,main函數(shù)棧還沒展開完成,,變量i還在棧中。但是在很多情況下,,block是作為參數(shù)傳遞以供后續(xù)回調(diào)執(zhí)行的,。通常在這些情況下,block被執(zhí)行時(shí),定義時(shí)所在的函數(shù)棧已經(jīng)被展開,,局部變量已經(jīng)不在棧中了(block此時(shí)在哪里,?),再用指針訪問就……,。 所以,,對于auto類型的局部變量,不允許block進(jìn)行修改是合理的,。 |
|