什么是塊對象
C編譯器和GCD
塊對象(block Object)不是Objective-C而是C語言的功能實現(xiàn)。在其他編程語言中,它與閉包(closure)功能相同。
塊對象的定義
塊對象的參數(shù)列和主體部分的書寫方法與普通函數(shù)相同。主體中如果有return,就可以定義返回值塊。格式如下:
^(參數(shù)列){主體}
從^開始到參數(shù)列,主體最后的大括號,這一段記述稱為塊對象的塊句法(block literal)。實際上,塊句法并不被用于在內(nèi)存中分配的塊對象,它只是編寫代碼時的一種表達用語。
塊對象本身常用于帶入到變量后評估,或被作為函數(shù)或方法的參數(shù)傳入等。此時,變量或參數(shù)的類型聲明和函數(shù)指針使用相同的書寫方法。只是函數(shù)指針聲明中使用“*”,而塊對象使用“^”。
塊對象和類型聲明
同函數(shù)指針一樣,為了簡化書寫,可以使用typedef簡化聲明。如:
typedef int (^myBlockType) (int);
使用該類型后,聲明函數(shù)func就可以使用下面的方式:
void func(myBlockType block);
如果塊對象沒有參數(shù),參數(shù)列則可以設置為(void),此外也可以將參數(shù)列連同括號全部省略,或者只保留括號。
此外,剋通過_ _BLOCKS_ _宏來檢查當前系統(tǒng)環(huán)境下是否可使用塊對象。根據(jù)編譯的不同條件,即可區(qū)別可以使用塊對象時的情況和不可以使用塊對象的情況。
塊對象中的變量行為
塊對象只在塊句法中保存自動變量的值。
塊對象就是把可以執(zhí)行的代碼和代碼中可訪問的變量“封裝”起來,使得之后可以做進一步處理的包。而閉包這個稱呼本身就是把變量等執(zhí)行環(huán)境封裝起來的意思。我們把閉包引用,讀取外部變量稱為捕獲(capture)。
總結:
塊句法主題中,除塊句法內(nèi)部的局部變量和形參外,還包括當前位置處可以訪問的變量。這些變量中有外部變量,還有包含塊句法的代碼塊內(nèi)可以訪問的局部變量。
從塊對象內(nèi)部可以直接訪問外部變量和靜態(tài)變量(static變量),也可以直接改變變量的值。
在包含塊句法的代碼塊內(nèi)可訪問的局部變量中,書寫句法塊時自動變量(棧內(nèi)變量)的值會被保存起來,然后再被訪問。
所以,即使自定變量最初的值發(fā)生了變化,塊對象在使用時也不會知道。
自動變量的值可以被讀取但是不能被改變。
自動變量為數(shù)組時,會發(fā)生編譯錯誤。
簡言之,在塊對象中雖然可以使用可訪問的變量,但自動變量的話就只能讀取復制值。換言之,自動變量在運行時就相當于const修飾的變量。
排序函數(shù)和塊對象
塊對象可以實現(xiàn)和函數(shù)指針相同的功能。使用函數(shù)指針時,需要寫不同的函數(shù)來應對各種不同的功能,此外,為了給函數(shù)傳遞需要的附加信息,往往還要使用多余的參數(shù)和外部變量,而這些都違背了編寫代碼要盡可能獨立易懂的原則。
通過靈活使用塊對象,我們就可以將這樣的函數(shù)或方法實現(xiàn)為易讀,靈活的書寫方式。
塊對象的構成
塊對象的實例和生命周期
在編譯塊句法時,會生成存放必要信息的內(nèi)存地址(實為結構體)和函數(shù)。變量中帶入的以及向函數(shù)傳入的實參,實際上就是這片內(nèi)存區(qū)域的指針。
在函數(shù)外部的塊句法被編譯后,塊對象的內(nèi)存區(qū)域就同外部變量一樣被配置在了靜態(tài)數(shù)據(jù)區(qū)中。
執(zhí)行包含塊句法的函數(shù)時,和自動變量相同,塊對象的內(nèi)存區(qū)域會在棧上得到分配。因此這些塊對象的生命周期也和自動變量相同,只在函數(shù)執(zhí)行期間存在。
#include
voidpr(int(^block)(void)) {
printf("%d\n", block());
}
int(^g)(void) = ^{return100; };
voidfunc1(intn) {
int(^b1)(void) = ^{returnn; };
pr(b1);
g = b1;
// assign the local block
}
voidfunc2(intn) {
inta =10;
int(^b2)(void) = ^{
returnn * a; };
pr(b2);
}
intmain(void)
{
pr(g);
func1(5);
func2(5);
pr(g); ?//
會發(fā)生于運行時錯誤
return0;
}
塊對象將要保存的自動變量的信息復制到了內(nèi)存區(qū)域。該內(nèi)存區(qū)域也包含了評估塊對象時所執(zhí)行的函數(shù)指針等的信息。
即使反復執(zhí)行塊句法處的代碼,也不會每次都為塊對象動態(tài)分配一片新的內(nèi)存區(qū)域。但是,被復制到內(nèi)存區(qū)域中的自動變量的值每次都會更新。另一方面,含塊句法的函數(shù)在遞歸調(diào)用時,同自動變量相同,塊對象就會在棧上保存多個內(nèi)存區(qū)域。
總結:
塊句法寫在函數(shù)外面時,只在靜態(tài)數(shù)據(jù)區(qū)分配一片內(nèi)存區(qū)域給塊對象。這片區(qū)域在程序執(zhí)行期會一直存在。
塊句法寫在函數(shù)內(nèi)時,和自動變量一樣,塊對象的內(nèi)存區(qū)域會在執(zhí)行包含塊對象的函數(shù)時被保存在棧上。該區(qū)域的生命周期就是在函數(shù)運行期間。
此外,在現(xiàn)在的實現(xiàn)中,當函數(shù)內(nèi)的塊句法不包含自動變量時,就沒必要復制值,所以塊對象會被設置在靜態(tài)數(shù)據(jù)區(qū)。但因為實現(xiàn)方法可能改變,應該避免編寫具有這種依賴關系的程序。
應該避免的編碼模式
上面注釋處代碼之所以會發(fā)生運行時錯誤,是因為棧上生成的塊對象在生命周期外是不能被使用的。
證明塊對象只有一個實體的實例:
voidfunc1(void) {
int i;
int (^blocks1[10])(void);
for(i =0; i <10; i++)
blocks1[i] = ^{returni; };
for(i =0; i <10; i++)
pr(blocks1[i]);
}
如果想為數(shù)組的各個元素代入不同的塊對象,就必須要進行下一節(jié)中所說的復制。但是,使用ARC時操作是不同的。
塊對象的復制
有一個函數(shù)可以復制塊對象到新的堆區(qū)域。通過使用該功能,即使是函數(shù)內(nèi)的塊對象也能獨立于棧被持續(xù)使用。此外,還有一個函數(shù)可以釋放不需要的塊對象。
Block_copy(block)
參數(shù)為棧上的塊對象時,返回堆上復制的塊對象。否則(參數(shù)為靜態(tài)數(shù)據(jù)區(qū)或為堆上的塊對象)則不進行復制而直接將參數(shù)返回,但會增加參數(shù)的塊對象的引用計數(shù)。
Block_release(block)
減少參數(shù)塊對象的引用計數(shù),減到0時釋放塊對象的內(nèi)存區(qū)域。
使用這些函數(shù)時,源文件中需要添加頭文件Block.h
如前所述,堆上分配的塊對象使用引用計數(shù)來管理。即使在使用垃圾回收的情況下,也必須成對調(diào)用Block_copy和Block_release。
指定特殊變量_ _block
通過_ _block修飾的變量有如下功能:
1.函數(shù)內(nèi)塊句法引用的_ _block變量是塊對象可以讀取的變量。同一個變量作用域內(nèi)有多個塊對象訪問時,他們之間可以共享_ _block變量的值。
2._ _block變量不是靜態(tài)變量,它在塊句法每次執(zhí)行塊句法時獲取變量的內(nèi)存區(qū)域。也就是說,同一個塊(變量作用域)內(nèi)的塊對象以及它們之間共享的_ _block變量是在執(zhí)行時動態(tài)生成的。
3.訪問_ _block變量的塊對象在被復制后,新生成的塊對象也能共享_ _block變量的值。
4.多個塊對象訪問一個_ _block變量時,只要有一個塊對象存在著,_ _block變量就會隨之存在。如果訪問_ _block變量的塊對象都不在了,_ _block也會隨之消失。
因為可能會涉及到實現(xiàn),這里省略了對_ _block變量行為的說明。但有一點,隨著塊對象的復制,_ _block的內(nèi)存位置會發(fā)生變化。而且不要寫使用指針來引用_ _block變量的代碼。
Objective和塊對象
方法定義和塊對象
塊對象作為方法參數(shù)傳遞時參數(shù)類型的指定方法:
現(xiàn)在假如有一個塊對象:BOOL(^block) (int,int) = ^(intindex,intlength){...;};
使用該塊對象作為參數(shù)的方法setBlock:聲明如下?!?BOOL(^)(int,int)) ”為參數(shù)類型
- (void)setBlock: (BOOL(^)(int,int)) block;
類型部分中也可以寫上形式參數(shù)名。
在OC中使用塊對象時,在塊句法內(nèi)也可以寫消息等OC的語法元素。
作為Objective-C對象的塊對象
OC程序在編譯運行時,塊對象會成為OC的對象來執(zhí)行操作。
有一點需要注意,retainCount方法返回的引用計數(shù)結果是不正確的。
ARC和塊對象
使用ARC和不使用ARC時,塊對象的操作是有區(qū)別的。
在ARC中,需要保存塊對象時,編譯器會自動插入copy操作。具體的說,就是在被帶入強訪問變量以及被作為return的返回值返回的時候。這些情況下,程序不需要顯式地執(zhí)行塊對象的副本。
但是,作為方法參數(shù)傳入的塊對象是不會自動執(zhí)行copy的。而且,當塊對象聲明為屬性值時,屬性選項一般會指定(copy)。
不要使用Block_copy和Block_release。因為已經(jīng)定義了(void *)型參數(shù)指針,所以ARC不能推測所有者。
_ _block變量的行為不同。不使用ARC時,_ _block變量只帶入值,示情況可能會懸空指針。ARC中因為有_ _strong修飾符修飾_ _block變量,使其作為強訪問變量來使用,因此就不會成為懸空指針。
對象內(nèi)變量的行為
介紹塊句法內(nèi)使用塊對象時的行為,特別是引用計數(shù)。
void(^cp)(void);
- (void)someMethod {
idobj = ...;
intn =10;
void(^block)(void) = ^{ [obj calc:n]; };
...
cp = [block copy];
}
如下圖a所示,塊對象在棧上生成,自動變量obj和n可以在塊內(nèi)使用。obj引用任意實例對象時,塊對象內(nèi)使用的變量obj也會訪問同一個對象。這時,變量的引用計數(shù)不會發(fā)生改變。
接下來塊對象自身被復制,并在堆區(qū)域中生成了新的塊對象。(圖b)。這里,實例對象的引用計數(shù)加1,由于方法執(zhí)行結束后自動變量obj也會消失,因此引用計數(shù)加1就使得塊對象成為了所有者。實例對象不是被復制,而是被共享,不只從塊對象,從哪都可以發(fā)送消息。
需要注意的是在某個類方法內(nèi)的塊句法被書寫了同一類的實例變量這種情況。如下面這個例子,假設ivar為包含方法someMethod的類實例變量。
void(^cp)(void);
- (void)someMethod {
intn =10;
void(^block)(void) = ^{ [ivar calc:n]; };
...
cp = [block copy];
}
這種情況下,當塊對象被復制時,self的引用計數(shù)將加1,而非ivar。如下圖所示,方法的引用參數(shù)self在堆上分配。在上例中,self好像沒有出現(xiàn)在塊句法中,我們可以按下面方式理解:
^{ [self->ivar calc:n]; };
塊句法內(nèi)的實例變量為整數(shù)或者實數(shù)時也是一樣的,self的引用計數(shù)也會增加。也就是說,當與self對等的對象不存在時,所有的實例變量都將不能訪問。
塊對象和對象的關系總結如下:?
方法定義內(nèi)的塊句法中存在實例變量時,可以直接訪問實例變量,也可以改變其值。
方法定義內(nèi)的塊句法中存在實例變量時,如果在棧上生成的塊對象的副本,retain就會被發(fā)送給self而非實例變量,引用計數(shù)器的值也會加1.實例變量的類型不一定非得是對象。
塊句法內(nèi)存在非實例變量的對象時,如果在棧上生成某個塊對象的副本,包含的對象就會接受到retain,引用計數(shù)器的值也會增加。
已經(jīng)復制后,堆區(qū)域中某個塊對象即使收到copy方法,結果也只是塊對象自身的引用計數(shù)器加1.包含的對象的引用計數(shù)器的值不變。
復制的塊對象在被釋放時,也會向包含的對象發(fā)送release。
ARC中使用塊對象時的注意事項?
使用ARC開發(fā)軟件時需要注意不要寫死循環(huán)代碼。使用塊對象時,相關對象可能會被自動保存,這時也許就會產(chǎn)生死循環(huán)。