在編程領域裏,一個牛逼程序員和一個二逼程序員之間的區別主要是其對所用編程語言優秀特性的運用方式。要說到Objective-C語言時,那麼通常開發者和大牛的區別可能就是對Block
書寫代碼的運用能力了。html
Block編程並非Objective-C語言首創的一個編程方式,Block也同時也以其餘的命名方式存在於其餘的編程語言中,例如在Javascript中閉包;Block首次於iOS 4.0版本中引入,其後便被普遍地接受和運用。在隨後的iOS版本中,爲了適用Block,Apple重寫了不少的framework方法。彷佛Block在必定程度上已經成爲了將來的一種編程方式。可是Block究竟是什麼呢?程序員
Block是一種添加到C、Objective-C和C++語言中的一個語言層面的特性,它容許您建立不一樣的代碼段,並像值同樣的傳遞到方法或函數中。Block是一個Objective-C對象,這就意味着其能夠被保存在NSArray或者NSDictionary中,Block還可以在本身的封閉做用域中截獲到值(即所謂的變量截獲),Block其實和其餘編程語言中的closure(閉包)或lambda是很相似的。編程
在定義Block的語法中咱們使用**脫字符(^)**來標識這是一個Block,以下所示:數組
^{
NSLog(@"This is a block");
}
複製代碼
與函數和方法定義同樣,大括號同時也表明着Block的開始與結束。 在這個例子中,Block不返回任何值,而且不接受任何參數。安全
與經過使用函數指針來引用C函數的相似方式,你也能夠經過聲明一個變量來記錄Block,如:bash
void (^simpleBlock)(void);
複製代碼
若是你對處理C語言的函數指針不熟悉,那麼上面的這種語法看起來會有點讓人摸不着頭腦。 上面的例子中聲明瞭一個名字爲simpleBlock的變量,用以引用一個沒有參數也沒有返回值的Block,這意味着這個Block變量能夠被最上面的Block所賦值,以下所示:多線程
simpleBlock = ^{
NSLog(@"This is a block");
};
複製代碼
這和任何其餘變量賦值同樣,因此語法上必須以大括號後面的分號做爲結束。 您也能夠將Block變量的聲明和賦值組合起來:閉包
void (^simpleBlock)(void) = ^{
NSLog(@"This is a block");
};
複製代碼
一旦Block被聲明且賦值後,您就能夠調用Block了,調用方法以下:併發
simpleBlock();
複製代碼
注意:若是你試圖調用一個沒有被賦值過的Block變量,你的應用會崩潰的。app
像方法和函數同樣,Block即接受參數也有返回值;例如,一個返回兩個值乘積的Block變量:
double (^multiplyTwoValues)(double, double);
複製代碼
對應於上面的Block變量,其相應的Block應該是這樣的:
^ (double firstValue, double secondValue) {
return firstValue * secondValue;
}
複製代碼
firstValue和secondValue用於引用在調用Block時提供的值,就像任何函數定義同樣。 在此示例中,返回類型是從Block內的return語句推斷的。
若是你喜歡,你能夠經過在脫字符(^)和參數列表之間指定來使返回類型顯式地寫出:
^ double (double firstValue, double secondValue) {
return firstValue * secondValue;
}
複製代碼
一旦你聲明和定義了Block,你就能夠像調用函數那樣調用Block:
double (^multiplyTwoValues)(double, double) =
^(double firstValue, double secondValue) {
return firstValue * secondValue;
};
double result = multiplyTwoValues(2,4);
NSLog(@"The result is %f", result);
複製代碼
除了包含可執行代碼以外,Block還具備從其封閉的做用域內截獲變量狀態的能力。
例如,若是在方法中聲明一個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(棧上Block的副本)都會引用該變量在堆上的副本。
你能夠像下面這樣重寫當前的例子:
__block int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock();
複製代碼
由於變量anInteger被聲明爲一個__block變量,它的內存地址與聲明中Block的變量地址是共享的。 這意味着日誌輸出如今將顯示:
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。 在平常代碼編寫中,一般將Block做爲參數傳遞給函數或方法以在其餘地方進行調用。 例如,您可使用GCD在後臺調用Block,或者定義一個要重複調用任務的Block,例如枚舉集合時。 併發和枚舉將在後面討論。
Block也用於回調,即定義任務完成時要執行的代碼。 例如,您的應用程序可能須要經過建立執行復雜任務的對象(例如從Web服務請求信息)來響應用戶操做。 由於任務可能須要很長時間,您應該在任務發生時顯示某種進度指示器(菊花),而後在任務完成後隱藏該指示器(菊花)。
固然,你可使用委託來完成這個任務:你須要建立一個合適的委託協議,實現所需的方法,將你的對象設置爲任務的委託,而後等待,一旦任務完成時它在你的對象上調用一個委託方法。
然而,Block可讓這些更加容易,由於您能夠在啓動任務時定義回調行爲,以下所示:
- (IBAction)fetchRemoteInformation:(id)sender {
[self showProgressIndicator];
XYZWebTask *task = ...
[task beginTaskWithCallbackBlock:^{
[self hideProgressIndicator];
}];
}
複製代碼
此示例調用一個方法來顯示進度指示器(菊花),而後建立任務並指示它開始。 回調Block指定任務完成後要執行的代碼; 在這種狀況下,它只是調用一個方法來隱藏進度指示器(菊花)。 注意,這個回調block截獲了self
,以便可以在調用時調用hideProgressIndicator
方法。 在截獲self
時要當心,由於它很容易建立一個strong
類型的循環引用,詳情見後面的如何在block截獲了self後避免循環引用。
在代碼可讀性方面,該Block使得在一個位置上很容易看到在任務完成以前和完成以後會發生哪些狀況,從而避免須要經過委託方法來查找將要發生的事情。
此示例中顯示的beginTaskWithCallbackBlock:
方法的聲明以下所示:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;
複製代碼
(void(^)(void))
上一個沒有參數沒有返回值的Block。 該方法的實現能夠以一般的方式調用Block:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
...
callbackBlock();
}
複製代碼
Block做爲方法的參數,其所擁有的多個或一個參數在形式上應與單純的Block變量相同:
- (void)doSomethingWithBlock:(void (^)(double, double))block {
...
block(21.0, 2.0);
}
複製代碼
若是方法中含有Block以及其餘非Block的參數, 那麼Block參數應該始終做爲方法的最後一個參數寫出,如:
- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;
複製代碼
這使得在指定Block內聯時更容易讀取方法的調用,以下所示:
[self beginTaskWithName:@"MyTask" completion:^{
NSLog(@"The task is complete");
}];
複製代碼
若是須要使用相同的Block類型來定義多個Block,您可能須要爲該類型進行從新的定義。 例如,您能夠爲沒有參數沒有返回值的簡單Block定義類型(即爲Block類型取一個別名):
typedef void (^XYZSimpleBlock)(void);
複製代碼
而後,可使用自定義類型的Block做爲方法的參數或用自定義類型來建立Block變量:
XYZSimpleBlock anotherBlock = ^{
...
};
- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
...
callbackBlock();
}
複製代碼
自定義類型定義在處理做爲返回值的Block或將其餘Block用做參數的Block時特別有用。 請看如下示例:
void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
...
return ^{
...
};
};
複製代碼
complexBlock變量指的是將另外一個Block做爲參數(aBlock)並返回另外一個Block的Block。 使用類型定義來重寫上面的代碼,這使的這段代碼更加可讀:
XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
...
return ^{
...
};
};
複製代碼
定義一個Block屬性的語法相似於聲明一個Block變量:
@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end
複製代碼
注意:您應該將copy
指定爲屬性修飾符,變量被Block截獲後,會改變自身在內存的位置,由棧區變爲堆區,因此Block也須要將本身複製到堆區,以應對這種改變。 當使用自動引用計數時,你是不須要擔憂的,由於它會自動發生的,可是屬性修飾符的最佳作法是顯示結果行爲。 有關更多信息,請參閱Blocks Programming Topics。
Block屬性的設置及調用和其餘的Block變量是同樣的:
self.blockProperty = ^{
...
};
self.blockProperty();
複製代碼
同時也可使用類型定義的方式聲明一個Block屬性,以下:
typedef void (^XYZSimpleBlock)(void);
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end
複製代碼
若是在定義一個Block回調時,須要在Block中截獲self
,內存管理的問題是須要引發重視的。
Block對任何截獲的對象都是強引用,包括self
;記住這一點後,想要解開循環引用就不是很難了,以下,一個擁有Block屬性的對象,在Block內截獲了self
:
@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
self.block = ^{
[self doSomething]; // Block對self是強引用的
// 這就產生了循環引用
};
}
...
@end
複製代碼
像上面這樣的一個簡單例子中,編譯器是會在你編寫代碼時報警告的;可是對於有多個強應用對象在一塊兒產生的循環引用問題,編譯器是很難發現循環引用問題的:
爲了不出現這種問題,最好的方式是截獲一個弱引用的self
,以下所示:
- (void)configureBlock {
XYZBlockKeeper * __weak weakSelf = self;
//或__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // 截獲一個弱引用self
// 以此來避免循環引用
}
}
複製代碼
經過在Block內截獲了一個弱指針指向的self
,這樣Block就不會再維持對XYZBlockKeeper對象的強引用關係了。若是對象在Block被調用以前釋放了,指針weakSelf
就會被置爲空;
除了做爲基本的回調使用外,許多的Cocoa 和 Cocoa Touch 框架的API也用Block來簡化任務,如集合枚舉。例如,NSArray就提供了三個含有Block的方法:
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
複製代碼
這個方法接受一個Block的參數,這個參數對於數組中的每一個項目調用一次:
NSArray *array = ...
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"Object at index %lu is %@", idx, obj);
}];
複製代碼
上面的Block須要三個參數,前兩個參數指向當前對象及其在數組中的索引。 第三個參數是一個指向布爾變量的指針,能夠用來中止枚舉,以下所示:
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
if (...) {
*stop = YES;
}
}];
複製代碼
還可使用enumerateObjectsWithOptions:usingBlock:
方法自定義枚舉。 例如,指定NSEnumerationReverse
這一選項將會反向遍歷集合。
若是枚舉Block中的代碼是處理器密集型(processor-intensive)而且是安全的併發執行 -- 您可使用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);
}];
複製代碼
如上面的例子所示:相比使用傳統的循環遍歷,使用枚舉鍵值對的方式會更加方便,
每一個Block表明一個不一樣的工做單元,就是可執行代碼與Block周圍做用域中截獲的可選狀態組合。 這使的Block成爲OS X和iOS中理想的異步併發調用可選項之一。 且無需弄清楚如何使用線程等低級機制,您可使用Block定義任務,而後讓系統在處理器資源可用時執行這些任務。
OS X和iOS提供了多種併發技術,包括兩種任務調度機制:Operation queues和GCD。 這些機制圍繞着一個等待被調用的任務隊列而設。 您按照須要調用它們的順序將Block添加到這一隊列中,當處理器時間和資源可用時,系統將對這一隊列中的Block進行調用。
串行隊列只容許一次執行一個任務 -- 隊列中的下一個任務直到前一個任務完成纔會被調用,在此期間這一任務將不會離開隊列。 併發隊列會調用盡量多的任務,而沒必要等待前面的任務完成。
操做隊列是Cocoa和Cocoa Touch框架的任務調度方式。 您建立一個NSOperation實例來封裝一個工做單元以及任何須要的數據,而後將該操做添加到NSOperationQueue中來執行。
雖然您能夠建立本身的自定義NSOperation子類來實現複雜的任務,但也能夠經過NSBlockOperation使用Block的方式建立一個操做,以下所示:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
...
}];
複製代碼
您能夠手動執行操做,但操做一般添加到現有的操做隊列或您本身建立的隊列中去執行:
// 在主隊列執行任務:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
// 在後臺隊列執行任務:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
複製代碼
若是使用操做隊列,能夠配置操做之間的優先級或依賴關係,例如指定一個操做先不執行,直到一組其餘操做完成才執行。例如,您還能夠經過KVO的方式監聽操做狀態的改變,而後在任務完成時,更新進度指示器(菊花):
更多關於操做和隊列操做的信息,見Operation Queues
若是須要安排任意Block代碼執行的話,您能夠直接使用由Grand Central Dispatch(GCD)控制的調度隊列(dispatch queues)。 調度隊列使得相對於調用者同步或異步地執行任務變得容易,而且以先進先出的順序執行它們的任務。
您能夠建立本身的調度隊列(dispatch queue)或使用GCD自動提供的隊列。 例如,若是須要安排併發執行的任務,能夠經過使用dispatch_get_global_queue()
函數並指定隊列優先級來獲取對現有隊列的引用,以下所示:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
//要將該block分派到隊列中,您可使用dispatch_async()或dispatch_sync()函數。
// dispatch_async()函數不會等待要調用的block執行完畢,而是當即返回:
dispatch_async(queue, ^{
NSLog(@"Block for asynchronous execution");
});
複製代碼
更多關於隊列調度和GCD的問題見Dispatch Queues.