block是C語言級別的語法和運行時特性,應用到Objective-C中可以增強函數(shù)功能。在合適場景中靈活應用block技術,對實際開發(fā)大有裨益。
block是對C語言中函數(shù)的擴展,除了函數(shù)中的代碼,還包含變量的綁定。block有時也被稱為閉包(closure),閉包就是一個函數(shù),或者一個指向函數(shù)的指針,加上這個函數(shù)執(zhí)行的非局部變量。通俗一點,就是閉包允許一個函數(shù)訪問聲明該函數(shù)運行上下文中的變量,甚至可以訪問不同運行上文中的變量。
腳本語言:
function?funA(callback){
alert(callback());
}
function?funB(){
var?str?=?"Hello?World";?//函數(shù)funB的局部變量,函數(shù)funA的非局部變量
funA(
function(){
return?str;
}
);
}
通過上面的代碼我們可以看出,按常規(guī)思維來說,變量str是函數(shù)funB的局部變量,作用域只在函數(shù)funB中,函數(shù)funA是無法訪問到str的。但是上述代碼示例中函數(shù)funA中的callback可以訪問到str,就是因為閉包性。
block實際上就是Objective-C語言對于閉包的實現(xiàn)。block配合dispatch_queue,可以方便地實現(xiàn)簡單的多線程編程和異步編程。
block原型及定義
block本質(zhì)上是和其他變量類似。不同的是block存儲的數(shù)據(jù)是一個函數(shù)體。使用block時,你可以像調(diào)用其他標準函數(shù)一樣,傳入?yún)?shù),并得到返回值。
脫字符(^)是block的語法標記,按照我們熟悉的參數(shù)語法規(guī)約所定義的返回值以及block的主體(也就是可以執(zhí)行的代碼)。下圖講解了如何把block變量賦值給一個變量的語法:
按照調(diào)用函數(shù)的方式調(diào)用block對象變量就可以了:
int?result?=?myBlock(4);?//result是28
使用typedef關鍵字
由于block數(shù)據(jù)類型的語法會降低整個代碼的閱讀性,所以常使用typedef來定義block類型。
typedef?double?(^Multiply2BlockRef)(double?c,?double?d);
這行語句定義了一個名為Multiply2BlockRef的block變量,它包含兩個double類型參數(shù)并返回一個double類型數(shù)值。有了typedef定義,就可以像下面這樣使用這個變量:
Multiply2BlockRef?multiply2?=?^(double?c,?double?d)?{
return?c?*?d;
}
printf(“%f”,?multiply2(4,?5));
Block和變量
block被聲明后會捕捉創(chuàng)建點時的狀態(tài)。block可以訪問函數(shù)用到的標準類型的變量:
全局變量;
全局函數(shù)(這個是可以調(diào)用);
封閉范圍內(nèi)的變量;
與block聲明時同級別的__block變量(這是可以修改的);
封閉范圍內(nèi)的非靜態(tài)變量會被獲取為常量;
Objective-C對象;
block內(nèi)部變量;
3.1 本地變量
本地變量就是與block在同一范圍內(nèi)聲明的變量。
typedef?double?(^Multiply2BlockRef)(double?c,?double?d);
double?a?=?10,?b?=?10;
Multiply2BlockRef?multiply?=?^(void)?{?return?a?*?b;}
a?=?20;
b?=?20;
NSLog(@“%f”,?multiply());
這個NSLog會輸出什么值?400?不是,為什么?
因為對于本地變量,block定義時copy變量的值,在block中作為常量使用,所以即使變量的值在block外改變,也不影響他在block中的值。NSLog只會輸出100。
3.2 全局變量
全局變量或靜態(tài)變量在內(nèi)存中的地址是固定的,block在讀取該變量值的時候是直接從其所在內(nèi)存讀出,獲取到的是最新值,而不是在定義時copy的常量。
3.3 Block變量
本地變量會被block作為常量獲取到,如果想要修改它們的值,必須將它們聲明為可修改的,否則會編譯出錯。
double?c?=?3;
Multiply2BlockRef?multiply2?=?^(double?a,?double?b){?c?=?a?*?b;?}?//?編譯會報錯
如果想要在block代碼里修改本地變量,需要將變量標記為__block。基于之前的代碼,給變量c添加__block關鍵字,如下:
__block?double?c?=?3;
對于用__block修飾的外部變量引用,block是復制其引用地址來實現(xiàn)訪問的。
3.4Objective-C對象
一般來說我們總會在設置block之后,在合適的時間回調(diào)block,而不希望回調(diào)block的時候block已經(jīng)被釋放了,所以我們需要對block進行copy,copy倒堆中,以便后用。
當一個block被Copy的時候,如果你在block里進行了一些調(diào)用,那么將會有一個強引用指向這些調(diào)用方法的調(diào)用者,有兩個規(guī)則:
如果是通過引用來訪問一個實例變量,那么將強引用至self;
如果是通過值來訪問一個實例變量,那么將直接強引用至這個“值”變量;
蘋果官方文檔里有兩個例子來說明這兩種情況:
dispatch_async(queue,?^{
//?instanceVariable?is?used?by?reference,?a?strong?reference?is?made?to?self
doSomethingWithObject(instanceVariable);
});
id?localVariable?=?instanceVariable;
dispatch_async(queue,?^{
/*
localVariable?is?used?by?value,?a?strong?reference?is?made?to?localVariable
(not?to?self)
*/
doSomethingWithObject(localVariable);
});
上面第一種情況相當于通過self.xxx來訪問實例變量,所以強引用指向了self;第二種情況把實例變量變成了本地臨時變量,強引用將直接指向這個本地的臨時變量。有時第一種情況可能會造成循環(huán)引用(在后面的注意事項中會解釋),要避免強引用到self的話,用__weak把self重新引用一下,在block中使用weakSelf就行了,比如:
__weak?UIViewController?*weakSelf?=?self;
Block類型
block有幾種不同的類型,這里列出常見的三種類型:
_NSConcreteGlobalBlock:全局的靜態(tài)block,不會訪問任何外部變量,不會涉及到任何拷貝,比如一個空的block。例如:
int main()
{
^{ printf("Hello, World!\n"); } ();
return 0;
}
_NSConcreteStackBlock:保存在棧中的block,當函數(shù)返回時被銷毀。例如:
int main()
{
char a = 'A';
^{ printf("%c\n",a); } ();
return 0;
}
_NSConcreteMallocBlock:保存在堆中的block,當引用計數(shù)為0時被銷毀。該類型的block都是由_NSConcreteStackBlock類型的block從棧中復制到堆中形成的。例如下面代碼中,在exampleB_addBlockToArray方法中的block還是_NSConcreteStackBlock類型的,在exampleB方法中就被復制到了堆中,成為_NSConcreteMallocBlock類型的block:
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%c\n", b);
}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
小結
_NSConcreteGlobalBlock類型的block要么是空block,要么是不訪問任何外部變量的block。它既不在棧中,也不在堆中,可以理解為它在內(nèi)存的text區(qū)。
_NSConcreteStackBlock類型的block有閉包行為,也就是有訪問外部變量,并且該block有且只有一次執(zhí)行,因為棧中的空間是可重復使用的,所以當棧中的block執(zhí)行一次之后就被清除出棧了,所以無法多次使用。
_NSConcreteMallocBlock類型的block有閉包行為,并且該block需要被多次執(zhí)行。當需要多次執(zhí)行時,就會把該block從棧中復制到堆中,供以多次執(zhí)行。
使用block的注意事項
避免在block里用self造成循環(huán)引用
block在copy時都會對block內(nèi)部用到的對象進行強引用(ARC)或者retainCount增加1(MRC)。在ARC與MRC環(huán)境下對block使用不當都會引起循環(huán)引用問題。一般表現(xiàn)為,某個類將block作為自己的屬性變量,然后該類在block的方法體里面又使用了該類本身,簡單說就是block的這種循環(huán)引用會被編譯器捕捉到并及時提醒。
self.someBlock?=?^(Type?var)?{
[self?doSomething];
//或self.otherVar?=?XXX;
//或_otherVar?=?...
};
即使在block代碼中沒有顯式地出現(xiàn)"self",也會出現(xiàn)循環(huán)引用!只要在block里用到了self所擁有的東西!
對于這種情況,可以通過添加__weak聲明(ARC)或者__block聲明(MRC)去禁止block對self進行強引用或者強制增加引用計數(shù)。
開發(fā)過程中該選擇block還是delegate
如果對象有超過一個以上不同的事件源,使用delegation;一般的delegate方法會有返回值;delegate的回調(diào)更多的面向過程,而block則是面向結果的。如果需要得到一條多步進程的通知,應該使用delegation。而只是希望得到請求的信息(或者獲取信息時的錯誤提示),應該使用block。
總結
可見靈活安全地使用block,必定會使編碼工作事半功倍。block經(jīng)常被用作回調(diào)函數(shù),取代傳統(tǒng)的回調(diào)方式,可以使得編碼時更順暢,不用中途換到另一個地方寫一個回調(diào)函數(shù)。采用block,可以在調(diào)用函數(shù)時直接寫后續(xù)處理代碼,將其作為參數(shù)傳遞過去,供其任務執(zhí)行結束時回調(diào)。block通常適用于以下場景:任務完成時回調(diào);處理消息監(jiān)聽回調(diào)處理;錯誤回調(diào)處理;枚舉回調(diào);視圖動畫、變換;排序等。
本文作者:張生輝(點融黑幫),來自點融北京技術團隊,曾在索貝做過3年C++開發(fā),目前主要做iOS開發(fā),對前端技術及新興技術比較感興趣。