在編程領(lǐng)域里噪裕,一個牛逼程序員和一個二逼程序員之間的區(qū)別主要是其對所用編程語言優(yōu)秀特性的運(yùn)用方式。要說到Objective-C語言時怎虫,那么一般開發(fā)者和大牛的區(qū)別可能就是對Block
書寫代碼的運(yùn)用能力了暑认。
Block編程并不是Objective-C語言獨(dú)創(chuàng)的一個編程方式,Block也同時也以其他的命名方式存在于其他的編程語言中大审,例如在Javascript中閉包蘸际;Block首次于iOS 4.0版本中引入,其后便被廣泛地接受和運(yùn)用徒扶。在隨后的iOS版本中粮彤,為了適用Block,Apple重寫了很多的framework方法姜骡。似乎Block在一定程度上已經(jīng)成為了未來的一種編程方式导坟。但是Block到底是什么呢?
Block是什么
Block是一種添加到C圈澈、Objective-C和C++語言中的一個語言層面的特性惫周,它允許您創(chuàng)建不同的代碼段,并像值一樣的傳遞到方法或函數(shù)中康栈。Block是一個Objective-C對象递递,這就意味著其可以被保存在NSArray或者NSDictionary中,Block還能夠在自己的封閉作用域中截獲到值(即所謂的變量截獲)啥么,Block其實和其他編程語言中的closure(閉包)或lambda是很類似的登舞。
Block 語法
在定義Block的語法中我們使用脫字符(^)來標(biāo)識這是一個Block,如下所示:
^{
NSLog(@"This is a block");
}
與函數(shù)和方法定義一樣饥臂,大括號同時也代表著Block的開始與結(jié)束逊躁。 在這個例子中,Block不返回任何值隅熙,并且不接受任何參數(shù)。
與通過使用函數(shù)指針來引用C函數(shù)的類似方式核芽,你也可以通過聲明一個變量來記錄Block囚戚,如:
void (^simpleBlock)(void);
如果你對處理C語言的函數(shù)指針不熟悉,那么上面的這種語法看起來會有點(diǎn)讓人摸不著頭腦轧简。 上面的例子中聲明了一個名字為simpleBlock的變量维苔,用以引用一個沒有參數(shù)也沒有返回值的Block桨武,這意味著這個Block變量可以被最上面的Block所賦值蚂维,如下所示:
simpleBlock = ^{
NSLog(@"This is a block");
};
這和任何其他變量賦值一樣命黔,所以語法上必須以大括號后面的分號作為結(jié)束。 您也可以將Block變量的聲明和賦值組合起來:
void (^simpleBlock)(void) = ^{
NSLog(@"This is a block");
};
一旦Block被聲明且賦值后籍茧,您就可以調(diào)用Block了,調(diào)用方法如下:
simpleBlock();
注意:如果你試圖調(diào)用一個沒有被賦值過的Block變量,你的應(yīng)用會崩潰的分飞。
Block的參數(shù)和返回值
像方法和函數(shù)一樣,Block即接受參數(shù)也有返回值睹限;例如譬猫,一個返回兩個值乘積的Block變量:
double (^multiplyTwoValues)(double, double);
對應(yīng)于上面的Block變量,其相應(yīng)的Block應(yīng)該是這樣的:
^ (double firstValue, double secondValue) {
return firstValue * secondValue;
}
firstValue和secondValue用于引用在調(diào)用Block時提供的值羡疗,就像任何函數(shù)定義一樣染服。 在此示例中,返回類型是從Block內(nèi)的return語句推斷的叨恨。
如果你喜歡柳刮,你可以通過在脫字符(^)和參數(shù)列表之間指定來使返回類型顯式地寫出:
^ double (double firstValue, double secondValue) {
return firstValue * secondValue;
}
一旦你聲明和定義了Block,你就可以像調(diào)用函數(shù)那樣調(diào)用Block:
double (^multiplyTwoValues)(double, double) =
^(double firstValue, double secondValue) {
return firstValue * secondValue;
};
double result = multiplyTwoValues(2,4);
NSLog(@"The result is %f", result);
Block可以截獲外部變量
除了包含可執(zhí)行代碼之外痒钝,Block還具有從其封閉的作用域內(nèi)截獲變量狀態(tài)的能力诚亚。
例如,如果在方法中聲明一個Block午乓,它可以截獲該方法作用域內(nèi)可訪問的任何變量的值站宗,如下所示:
- (void)testMethod {
int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
testBlock();
}
在此示例中,anInteger是在Block之外聲明的一個變量益愈,但是Block卻在定義時截獲了變量的值梢灭。
int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock();
Block截獲的變量的值沒有改變。 這意味著日志的輸出將顯示為:
Integer is: 42
這也意味著Block不能改變原始變量的值蒸其,甚至是截獲變量值(被截獲的變量變成了一個常量)敏释。
__block修飾的變量
當(dāng)一個Block被復(fù)制后(當(dāng)Block截獲到外部變量時,Block就會被復(fù)制到堆上)摸袁,__block
聲明的棧變量的引用也會被復(fù)制到了堆里钥顽,復(fù)制完成之后,無論是棧上的Block還是剛剛產(chǎn)生在堆上的Block(棧上Block的副本)都會引用該變量在堆上的副本靠汁。
你可以像下面這樣重寫當(dāng)前的例子:
__block int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock();
因為變量anInteger被聲明為一個__block變量蜂大,它的內(nèi)存地址與聲明中Block的變量地址是共享的。 這意味著日志輸出現(xiàn)在將顯示:
Integer is: 84
這同時也標(biāo)志著Block可以修改其變量的原始值蝶怔,如下所示:
__block int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
anInteger = 100;
};
testBlock();
NSLog(@"Value of original variable is now: %i", anInteger);
這次的輸出會是:
Integer is: 42
Value of original variable is now: 100
Block作為方法或函數(shù)的參數(shù)
前面的每個例子都是在定義之后會立即調(diào)用Block奶浦。 在日常代碼編寫中,通常將Block作為參數(shù)傳遞給函數(shù)或方法以在其他地方進(jìn)行調(diào)用踢星。 例如澳叉,您可以使用GCD在后臺調(diào)用Block,或者定義一個要重復(fù)調(diào)用任務(wù)的Block,例如枚舉集合時成洗。 并發(fā)和枚舉將在后面討論五督。
Block也用于回調(diào),即定義任務(wù)完成時要執(zhí)行的代碼瓶殃。 例如充包,您的應(yīng)用程序可能需要通過創(chuàng)建執(zhí)行復(fù)雜任務(wù)的對象(例如從Web服務(wù)請求信息)來響應(yīng)用戶操作。 因為任務(wù)可能需要很長時間碌燕,您應(yīng)該在任務(wù)發(fā)生時顯示某種進(jìn)度指示器(菊花)误证,然后在任務(wù)完成后隱藏該指示器(菊花)。
當(dāng)然修壕,你可以使用委托來完成這個任務(wù):你需要創(chuàng)建一個合適的委托協(xié)議愈捅,實現(xiàn)所需的方法,將你的對象設(shè)置為任務(wù)的委托慈鸠,然后等待蓝谨,一旦任務(wù)完成時它在你的對象上調(diào)用一個委托方法。
然而青团,Block可以讓這些更加容易譬巫,因為您可以在啟動任務(wù)時定義回調(diào)行為,如下所示:
- (IBAction)fetchRemoteInformation:(id)sender {
[self showProgressIndicator];
XYZWebTask *task = ...
[task beginTaskWithCallbackBlock:^{
[self hideProgressIndicator];
}];
}
此示例調(diào)用一個方法來顯示進(jìn)度指示器(菊花)督笆,然后創(chuàng)建任務(wù)并指示它開始芦昔。 回調(diào)Block指定任務(wù)完成后要執(zhí)行的代碼; 在這種情況下,它只是調(diào)用一個方法來隱藏進(jìn)度指示器(菊花)娃肿。 注意咕缎,這個回調(diào)block截獲了self
,以便能夠在調(diào)用時調(diào)用hideProgressIndicator
方法料扰。 在截獲self
時要小心凭豪,因為它很容易創(chuàng)建一個strong
類型的循環(huán)引用,詳情見后面的如何在block截獲了self后避免循環(huán)引用晒杈。
在代碼可讀性方面嫂伞,該Block使得在一個位置上很容易看到在任務(wù)完成之前和完成之后會發(fā)生哪些情況,從而避免需要通過委托方法來查找將要發(fā)生的事情拯钻。
此示例中顯示的beginTaskWithCallbackBlock:
方法的聲明如下所示:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;
(void(^)(void))
上一個沒有參數(shù)沒有返回值的Block帖努。 該方法的實現(xiàn)可以以通常的方式調(diào)用Block:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
...
callbackBlock();
}
Block作為方法的參數(shù),其所擁有的多個或一個參數(shù)在形式上應(yīng)與單純的Block變量相同:
- (void)doSomethingWithBlock:(void (^)(double, double))block {
...
block(21.0, 2.0);
}
Block應(yīng)該始終作為方法的最后一個參數(shù)
如果方法中含有Block以及其他非Block的參數(shù)说庭, 那么Block參數(shù)應(yīng)該始終作為方法的最后一個參數(shù)寫出然磷,如:
- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;
這使得在指定Block內(nèi)聯(lián)時更容易讀取方法的調(diào)用,如下所示:
[self beginTaskWithName:@"MyTask" completion:^{
NSLog(@"The task is complete");
}];
使用類型定義來簡化Block語法
如果需要使用相同的Block類型來定義多個Block刊驴,您可能需要為該類型進(jìn)行重新的定義。
例如,您可以為沒有參數(shù)沒有返回值的簡單Block定義類型(即為Block類型取一個別名):
typedef void (^XYZSimpleBlock)(void);
然后捆憎,可以使用自定義類型的Block作為方法的參數(shù)或用自定義類型來創(chuàng)建Block變量:
XYZSimpleBlock anotherBlock = ^{
...
};
- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
...
callbackBlock();
}
自定義類型定義在處理作為返回值的Block或?qū)⑵渌鸅lock用作參數(shù)的Block時特別有用舅柜。 請看以下示例:
void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
...
return ^{
...
};
};
complexBlock變量指的是將另一個Block作為參數(shù)(aBlock)并返回另一個Block的Block。
使用類型定義來重寫上面的代碼躲惰,這使的這段代碼更加可讀:
XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
...
return ^{
...
};
};
對象使用Block作為屬性
定義一個Block屬性的語法類似于聲明一個Block變量:
@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end
注意:您應(yīng)該將copy
指定為屬性修飾符致份,變量被Block截獲后,會改變自身在內(nèi)存的位置础拨,由棧區(qū)變?yōu)槎褏^(qū)氮块,所以Block也需要將自己復(fù)制到堆區(qū),以應(yīng)對這種改變诡宗。 當(dāng)使用自動引用計數(shù)時滔蝉,你是不需要擔(dān)心的,因為它會自動發(fā)生的塔沃,但是屬性修飾符的最佳做法是顯示結(jié)果行為蝠引。 有關(guān)更多信息,請參閱Blocks Programming Topics蛀柴。
Block屬性的設(shè)置及調(diào)用和其他的Block變量是一樣的:
self.blockProperty = ^{
...
};
self.blockProperty();
同時也可以使用類型定義的方式聲明一個Block屬性螃概,如下:
typedef void (^XYZSimpleBlock)(void);
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end
<a id="no1"></a>如何在Block截獲了self后避免循環(huán)引用
如果在定義一個Block回調(diào)時,需要在Block中截獲self
鸽疾,內(nèi)存管理的問題是需要引起重視的吊洼。
Block對任何截獲的對象都是強(qiáng)引用,包括self
制肮;記住這一點(diǎn)后冒窍,想要解開循環(huán)引用就不是很難了,如下弄企,一個擁有Block屬性的對象超燃,在Block內(nèi)截獲了self
:
@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
self.block = ^{
[self doSomething]; // Block對self是強(qiáng)引用的
// 這就產(chǎn)生了循環(huán)引用
};
}
...
@end
像上面這樣的一個簡單例子中,編譯器是會在你編寫代碼時報警告的拘领;但是對于有多個強(qiáng)應(yīng)用對象在一起產(chǎn)生的循環(huán)引用問題意乓,編譯器是很難發(fā)現(xiàn)循環(huán)引用問題的:
為了避免出現(xiàn)這種問題,最好的方式是截獲一個弱引用的self
约素,如下所示:
- (void)configureBlock {
XYZBlockKeeper * __weak weakSelf = self;
//或__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // 截獲一個弱引用self
// 以此來避免循環(huán)引用
}
}
通過在Block內(nèi)截獲了一個弱指針指向的self
届良,這樣Block就不會再維持對XYZBlockKeeper對象的強(qiáng)引用關(guān)系了。如果對象在Block被調(diào)用之前釋放了圣猎,指針weakSelf
就會被置為空士葫;
Block可以用來簡化枚舉
除了作為基本的回調(diào)使用外,許多的Cocoa 和 Cocoa Touch 框架的API也用Block來簡化任務(wù)送悔,如集合枚舉慢显。例如爪模,NSArray就提供了三個含有Block的方法:
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
這個方法接受一個Block的參數(shù),這個參數(shù)對于數(shù)組中的每個項目調(diào)用一次:
NSArray *array = ...
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"Object at index %lu is %@", idx, obj);
}];
上面的Block需要三個參數(shù)荚藻,前兩個參數(shù)指向當(dāng)前對象及其在數(shù)組中的索引屋灌。 第三個參數(shù)是一個指向布爾變量的指針,可以用來停止枚舉应狱,如下所示:
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
if (...) {
*stop = YES;
}
}];
還可以使用enumerateObjectsWithOptions:usingBlock:
方法自定義枚舉共郭。 例如,指定NSEnumerationReverse
這一選項將會反向遍歷集合疾呻。
如果枚舉Block中的代碼是處理器密集型(processor-intensive)并且是安全的并發(fā)執(zhí)行 -- 您可以使用NSEnumerationConcurrent
選項:
[array enumerateObjectsWithOptions:NSEnumerationConcurrent
usingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
...
}];
這個flag指示Block枚舉的調(diào)用可能會是多線程分布的除嘹,如果Block代碼是專門針對處理器密集型的,那么這樣做對性能會有潛在的提升。注意岸蜗,當(dāng)使用這個選項時尉咕,這個枚舉的順序是未定義的。
NSDictionary同時也提供一些基于Block的方法散吵,如下所示:
NSDictionary *dictionary = ...
[dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) {
NSLog(@"key: %@, value: %@", key, obj);
}];
如上面的例子所示:相比使用傳統(tǒng)的循環(huán)遍歷龙考,使用枚舉鍵值對的方式會更加方便,
Block可以用來簡化并發(fā)任務(wù)
每個Block代表一個不同的工作單元矾睦,就是可執(zhí)行代碼與Block周圍作用域中截獲的可選狀態(tài)組合晦款。 這使的Block成為OS X和iOS中理想的異步并發(fā)調(diào)用可選項之一。 且無需弄清楚如何使用線程等低級機(jī)制枚冗,您可以使用Block定義任務(wù)缓溅,然后讓系統(tǒng)在處理器資源可用時執(zhí)行這些任務(wù)。
OS X和iOS提供了多種并發(fā)技術(shù)赁温,包括兩種任務(wù)調(diào)度機(jī)制:Operation queues和GCD坛怪。 這些機(jī)制圍繞著一個等待被調(diào)用的任務(wù)隊列而設(shè)。 您按照需要調(diào)用它們的順序?qū)lock添加到這一隊列中股囊,當(dāng)處理器時間和資源可用時袜匿,系統(tǒng)將對這一隊列中的Block進(jìn)行調(diào)用。
串行隊列只允許一次執(zhí)行一個任務(wù) -- 隊列中的下一個任務(wù)直到前一個任務(wù)完成才會被調(diào)用稚疹,在此期間這一任務(wù)將不會離開隊列居灯。 并發(fā)隊列會調(diào)用盡可能多的任務(wù),而不必等待前面的任務(wù)完成内狗。
使用Block操作隊列
操作隊列是Cocoa和Cocoa Touch框架的任務(wù)調(diào)度方式怪嫌。 您創(chuàng)建一個NSOperation實例來封裝一個工作單元以及任何必要的數(shù)據(jù),然后將該操作添加到NSOperationQueue中來執(zhí)行柳沙。
雖然您可以創(chuàng)建自己的自定義NSOperation子類來實現(xiàn)復(fù)雜的任務(wù)岩灭,但也可以通過NSBlockOperation使用Block的方式創(chuàng)建一個操作,如下所示:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
...
}];
您可以手動執(zhí)行操作赂鲤,但操作通常添加到現(xiàn)有的操作隊列或您自己創(chuàng)建的隊列中去執(zhí)行:
// 在主隊列執(zhí)行任務(wù):
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
// 在后臺隊列執(zhí)行任務(wù):
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
如果使用操作隊列噪径,可以配置操作之間的優(yōu)先級或依賴關(guān)系柱恤,例如指定一個操作先不執(zhí)行,直到一組其他操作完成才執(zhí)行熄云。例如膨更,您還可以通過KVO的方式監(jiān)聽操作狀態(tài)的改變妙真,然后在任務(wù)完成時缴允,更新進(jìn)度指示器(菊花):
更多關(guān)于操作和隊列操作的信息,見Operation Queues
使用GCD在調(diào)度隊列中給Block進(jìn)行進(jìn)度安排珍德。
如果需要安排任意Block代碼執(zhí)行的話练般,您可以直接使用由Grand Central Dispatch(GCD)控制的調(diào)度隊列(dispatch queues)。 調(diào)度隊列使得相對于調(diào)用者同步或異步地執(zhí)行任務(wù)變得容易锈候,并且以先進(jìn)先出的順序執(zhí)行它們的任務(wù)薄料。
您可以創(chuàng)建自己的調(diào)度隊列(dispatch queue)或使用GCD自動提供的隊列。 例如泵琳,如果需要安排并發(fā)執(zhí)行的任務(wù)摄职,可以通過使用dispatch_get_global_queue()
函數(shù)并指定隊列優(yōu)先級來獲取對現(xiàn)有隊列的引用,如下所示:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
//要將該block分派到隊列中获列,您可以使用dispatch_async()或dispatch_sync()函數(shù)谷市。
// dispatch_async()函數(shù)不會等待要調(diào)用的block執(zhí)行完畢,而是立即返回:
dispatch_async(queue, ^{
NSLog(@"Block for asynchronous execution");
});
更多關(guān)于隊列調(diào)度和GCD的問題見Dispatch Queues.