在編程領域里,一個牛逼程序員和一個二逼程序員之間的區(qū)別主要是其對所用編程語言優(yōu)秀特性的運用方式。要說到Objective-C語言時,那么一般開發(fā)者和大牛的區(qū)別可能就是對Block
書寫代碼的運用能力了。
Block編程并不是Objective-C語言獨創(chuàng)的一個編程方式,Block也同時也以其他的命名方式存在于其他的編程語言中,例如在Javascript中閉包;Block首次于iOS 4.0版本中引入,其后便被廣泛地接受和運用。在隨后的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的語法中我們使用脫字符(^)來標識這是一個Block,如下所示:
^{
NSLog(@"This is a block");
}
與函數(shù)和方法定義一樣,大括號同時也代表著Block的開始與結束。 在這個例子中,Block不返回任何值,并且不接受任何參數(shù)。
與通過使用函數(shù)指針來引用C函數(shù)的類似方式,你也可以通過聲明一個變量來記錄Block,如:
void (^simpleBlock)(void);
如果你對處理C語言的函數(shù)指針不熟悉,那么上面的這種語法看起來會有點讓人摸不著頭腦。 上面的例子中聲明了一個名字為simpleBlock的變量,用以引用一個沒有參數(shù)也沒有返回值的Block,這意味著這個Block變量可以被最上面的Block所賦值,如下所示:
simpleBlock = ^{
NSLog(@"This is a block");
};
這和任何其他變量賦值一樣,所以語法上必須以大括號后面的分號作為結束。 您也可以將Block變量的聲明和賦值組合起來:
void (^simpleBlock)(void) = ^{
NSLog(@"This is a block");
};
一旦Block被聲明且賦值后,您就可以調用Block了,調用方法如下:
simpleBlock();
注意:如果你試圖調用一個沒有被賦值過的Block變量,你的應用會崩潰的。
Block的參數(shù)和返回值
像方法和函數(shù)一樣,Block即接受參數(shù)也有返回值;例如,一個返回兩個值乘積的Block變量:
double (^multiplyTwoValues)(double, double);
對應于上面的Block變量,其相應的Block應該是這樣的:
^ (double firstValue, double secondValue) {
return firstValue * secondValue;
}
firstValue和secondValue用于引用在調用Block時提供的值,就像任何函數(shù)定義一樣。 在此示例中,返回類型是從Block內的return語句推斷的。
如果你喜歡,你可以通過在脫字符(^)和參數(shù)列表之間指定來使返回類型顯式地寫出:
^ double (double firstValue, double secondValue) {
return firstValue * secondValue;
}
一旦你聲明和定義了Block,你就可以像調用函數(shù)那樣調用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還具有從其封閉的作用域內截獲變量狀態(tài)的能力。
例如,如果在方法中聲明一個Block,它可以截獲該方法作用域內可訪問的任何變量的值,如下所示:
- (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修飾的變量
當一個Block被復制后(當Block截獲到外部變量時,Block就會被復制到堆上),__block
聲明的棧變量的引用也會被復制到了堆里,復制完成之后,無論是棧上的Block還是剛剛產(chǎn)生在堆上的Block(棧上Block的副本)都會引用該變量在堆上的副本。
你可以像下面這樣重寫當前的例子:
__block int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock();
因為變量anInteger被聲明為一個__block變量,它的內存地址與聲明中Block的變量地址是共享的。 這意味著日志輸出現(xiàn)在將顯示:
Integer is: 84
這同時也標志著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ù)
前面的每個例子都是在定義之后會立即調用Block。 在日常代碼編寫中,通常將Block作為參數(shù)傳遞給函數(shù)或方法以在其他地方進行調用。 例如,您可以使用GCD在后臺調用Block,或者定義一個要重復調用任務的Block,例如枚舉集合時。 并發(fā)和枚舉將在后面討論。
Block也用于回調,即定義任務完成時要執(zhí)行的代碼。 例如,您的應用程序可能需要通過創(chuàng)建執(zhí)行復雜任務的對象(例如從Web服務請求信息)來響應用戶操作。 因為任務可能需要很長時間,您應該在任務發(fā)生時顯示某種進度指示器(菊花),然后在任務完成后隱藏該指示器(菊花)。
當然,你可以使用委托來完成這個任務:你需要創(chuàng)建一個合適的委托協(xié)議,實現(xiàn)所需的方法,將你的對象設置為任務的委托,然后等待,一旦任務完成時它在你的對象上調用一個委托方法。
然而,Block可以讓這些更加容易,因為您可以在啟動任務時定義回調行為,如下所示:
- (IBAction)fetchRemoteInformation:(id)sender {
[self showProgressIndicator];
XYZWebTask *task = ...
[task beginTaskWithCallbackBlock:^{
[self hideProgressIndicator];
}];
}
此示例調用一個方法來顯示進度指示器(菊花),然后創(chuàng)建任務并指示它開始。 回調Block指定任務完成后要執(zhí)行的代碼; 在這種情況下,它只是調用一個方法來隱藏進度指示器(菊花)。 注意,這個回調block截獲了self
,以便能夠在調用時調用hideProgressIndicator
方法。 在截獲self
時要小心,因為它很容易創(chuàng)建一個strong
類型的循環(huán)引用,詳情見后面的如何在block截獲了self后避免循環(huán)引用。
在代碼可讀性方面,該Block使得在一個位置上很容易看到在任務完成之前和完成之后會發(fā)生哪些情況,從而避免需要通過委托方法來查找將要發(fā)生的事情。
此示例中顯示的beginTaskWithCallbackBlock:
方法的聲明如下所示:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;
(void(^)(void))
上一個沒有參數(shù)沒有返回值的Block。 該方法的實現(xiàn)可以以通常的方式調用Block:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
...
callbackBlock();
}
Block作為方法的參數(shù),其所擁有的多個或一個參數(shù)在形式上應與單純的Block變量相同:
- (void)doSomethingWithBlock:(void (^)(double, double))block {
...
block(21.0, 2.0);
}
Block應該始終作為方法的最后一個參數(shù)
如果方法中含有Block以及其他非Block的參數(shù), 那么Block參數(shù)應該始終作為方法的最后一個參數(shù)寫出,如:
- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;
這使得在指定Block內聯(lián)時更容易讀取方法的調用,如下所示:
[self beginTaskWithName:@"MyTask" completion:^{
NSLog(@"The task is complete");
}];
使用類型定義來簡化Block語法
如果需要使用相同的Block類型來定義多個Block,您可能需要為該類型進行重新的定義。
例如,您可以為沒有參數(shù)沒有返回值的簡單Block定義類型(即為Block類型取一個別名):
typedef void (^XYZSimpleBlock)(void);
然后,可以使用自定義類型的Block作為方法的參數(shù)或用自定義類型來創(chuàng)建Block變量:
XYZSimpleBlock anotherBlock = ^{
...
};
- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
...
callbackBlock();
}
自定義類型定義在處理作為返回值的Block或將其他Block用作參數(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
注意:您應該將copy
指定為屬性修飾符,變量被Block截獲后,會改變自身在內存的位置,由棧區(qū)變?yōu)槎褏^(qū),所以Block也需要將自己復制到堆區(qū),以應對這種改變。 當使用自動引用計數(shù)時,你是不需要擔心的,因為它會自動發(fā)生的,但是屬性修飾符的最佳做法是顯示結果行為。 有關更多信息,請參閱Blocks Programming Topics。
Block屬性的設置及調用和其他的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回調時,需要在Block中截獲self
,內存管理的問題是需要引起重視的。
Block對任何截獲的對象都是強引用,包括self
;記住這一點后,想要解開循環(huán)引用就不是很難了,如下,一個擁有Block屬性的對象,在Block內截獲了self
:
@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
self.block = ^{
[self doSomething]; // Block對self是強引用的
// 這就產(chǎn)生了循環(huán)引用
};
}
...
@end
像上面這樣的一個簡單例子中,編譯器是會在你編寫代碼時報警告的;但是對于有多個強應用對象在一起產(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內截獲了一個弱指針指向的self
,這樣Block就不會再維持對XYZBlockKeeper對象的強引用關系了。如果對象在Block被調用之前釋放了,指針weakSelf
就會被置為空;
Block可以用來簡化枚舉
除了作為基本的回調使用外,許多的Cocoa 和 Cocoa Touch 框架的API也用Block來簡化任務,如集合枚舉。例如,NSArray就提供了三個含有Block的方法:
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
這個方法接受一個Block的參數(shù),這個參數(shù)對于數(shù)組中的每個項目調用一次:
NSArray *array = ...
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"Object at index %lu is %@", idx, obj);
}];
上面的Block需要三個參數(shù),前兩個參數(shù)指向當前對象及其在數(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枚舉的調用可能會是多線程分布的,如果Block代碼是專門針對處理器密集型的,那么這樣做對性能會有潛在的提升。注意,當使用這個選項時,這個枚舉的順序是未定義的。
NSDictionary同時也提供一些基于Block的方法,如下所示:
NSDictionary *dictionary = ...
[dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) {
NSLog(@"key: %@, value: %@", key, obj);
}];
如上面的例子所示:相比使用傳統(tǒng)的循環(huán)遍歷,使用枚舉鍵值對的方式會更加方便,
Block可以用來簡化并發(fā)任務
每個Block代表一個不同的工作單元,就是可執(zhí)行代碼與Block周圍作用域中截獲的可選狀態(tài)組合。 這使的Block成為OS X和iOS中理想的異步并發(fā)調用可選項之一。 且無需弄清楚如何使用線程等低級機制,您可以使用Block定義任務,然后讓系統(tǒng)在處理器資源可用時執(zhí)行這些任務。
OS X和iOS提供了多種并發(fā)技術,包括兩種任務調度機制:Operation queues和GCD。 這些機制圍繞著一個等待被調用的任務隊列而設。 您按照需要調用它們的順序將Block添加到這一隊列中,當處理器時間和資源可用時,系統(tǒng)將對這一隊列中的Block進行調用。
串行隊列只允許一次執(zhí)行一個任務 -- 隊列中的下一個任務直到前一個任務完成才會被調用,在此期間這一任務將不會離開隊列。 并發(fā)隊列會調用盡可能多的任務,而不必等待前面的任務完成。
使用Block操作隊列
操作隊列是Cocoa和Cocoa Touch框架的任務調度方式。 您創(chuàng)建一個NSOperation實例來封裝一個工作單元以及任何必要的數(shù)據(jù),然后將該操作添加到NSOperationQueue中來執(zhí)行。
雖然您可以創(chuàng)建自己的自定義NSOperation子類來實現(xiàn)復雜的任務,但也可以通過NSBlockOperation使用Block的方式創(chuàng)建一個操作,如下所示:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
...
}];
您可以手動執(zhí)行操作,但操作通常添加到現(xiàn)有的操作隊列或您自己創(chuàng)建的隊列中去執(zhí)行:
// 在主隊列執(zhí)行任務:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
// 在后臺隊列執(zhí)行任務:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
如果使用操作隊列,可以配置操作之間的優(yōu)先級或依賴關系,例如指定一個操作先不執(zhí)行,直到一組其他操作完成才執(zhí)行。例如,您還可以通過KVO的方式監(jiān)聽操作狀態(tài)的改變,然后在任務完成時,更新進度指示器(菊花):
更多關于操作和隊列操作的信息,見Operation Queues
使用GCD在調度隊列中給Block進行進度安排。
如果需要安排任意Block代碼執(zhí)行的話,您可以直接使用由Grand Central Dispatch(GCD)控制的調度隊列(dispatch queues)。 調度隊列使得相對于調用者同步或異步地執(zhí)行任務變得容易,并且以先進先出的順序執(zhí)行它們的任務。
您可以創(chuàng)建自己的調度隊列(dispatch queue)或使用GCD自動提供的隊列。 例如,如果需要安排并發(fā)執(zhí)行的任務,可以通過使用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ù)不會等待要調用的block執(zhí)行完畢,而是立即返回:
dispatch_async(queue, ^{
NSLog(@"Block for asynchronous execution");
});
更多關于隊列調度和GCD的問題見Dispatch Queues.