版權聲明:原創作品,謝絕轉載!否則將追究法律責任。
一個Objective-c類定義了一個對象結合數據相關的行為。有時候,這使得他有意義的表達單個任務或者單元的行為。而不是集合的方法。
blocks是語言的特性,我們可以在C C++ 和Objective-c看到,這允許你創建不同的代碼片段,這代碼片段可以通過在方法或者函數里調用如果他們有值。blocks是Objective-c的對象,意味着他們可以被添加到集合像數組和字典里。他們也有能力撲捉封閉范圍值。
這章我們闡述怎么聲明和引用blocks的語法,和展示怎么用塊語法來簡化我們通常的任務例如集合的枚舉。更多信息參考: Blocks Programming Topics.
blocks的語法:
用(^)定義blocks字面語法像這樣:
^{
NSLog(@"This is a block");
}
就像函數和方法定義那樣,{}表示函數的開頭和結尾。這個例子中block沒有返回值也沒有參數。
同樣的你也可以用函數指針來引用一個C函數,你可以聲明一個變量來跟蹤block像這樣:
void (^simpleBlock)(void);
如果你不習慣處理C函數指針,語法可能似乎有點不尋常,這個例子叫做simpleBlock聲明了一個變量來引用一個塊不帶任何參數不返回值。這意味着塊可以指定給變量像這樣:
simpleBlock = ^{
NSLog(@"This is a block");
};
這就像其他任何變量的賦值,因此語句必須以分號結束。你可以把變量聲明和賦值放在一起:
void (^simpleBlock)(void) = ^{
NSLog(@"This is a block");
};
一旦你已經聲明和賦值一個block變量,你可以調用block:
simpleBlock();
注意:注意你聲明的塊變量沒有賦值為nil你的應用會崩潰。
塊帶着參數和返回值:
塊也可以帶參數和返回值就像方法和函數一樣。
舉個例子把兩個值相乘的結果返回給一個塊變量:
double (^multiplyTwoValues)(double, double);
相應的字面塊語法像這樣:
^ (double firstValue, double secondValue) {
return firstValue * secondValue;
}
當塊調用的時候這個firstValue 和secondValue引用的值。就像任何函數定義一樣。這個例子中這個返回值類型是塊語句返回值類型推斷的。
你也可以顯示的聲明返回的類型在^和()之前像這樣:
^ double (double firstValue, double secondValue) {
return firstValue * secondValue;
}
一旦你聲明和定義了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定義的時候值被撲捉。
一旦值被撲捉,除非你特別說明。這意味着在你定義block和調用他之間如果你改變外部變量值。就像這樣:
int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock();
這個值被block撲捉沒有收到影響。這意味着打印的值是
Integer is: 42
這意味着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
你可以把blocks作為方法或者函數的參數傳遞:
之前在這章的每個例子定義過block后馬上被調用。事實上,這是常用的在其他地方調用塊函數或者方法。你可以在后台用GCD調用block,例如,或者定義一個block代表一個任務被調用多次,例如當枚舉集合。並發性或者枚舉性在后面章節介紹。
blocks可以被用來回調,定義代碼被執行在任務完成時候。例如你的應用可能響應用戶創建對象的動作來執行一個完成的任務。例如請求信息從服務器。因為這個任務可能用很長時間,當任務發生的時候你可以讓一個指示器來表示正在請求數據,一旦任務完成的時候你隱藏指示器。
你也可能完成這個任務用代理:你需要創建一個合適的協議,實現必須的方法,設置你的對象作為任務的委托,然后你等着代理方法被調用一旦你的對象完成了方法。
blocks可以使這變的更簡單,因為你可以定義回調行為當你發起任務時候像這樣:
- (IBAction)fetchRemoteInformation:(id)sender {
[self showProgressIndicator];
XYZWebTask *task = ...
[task beginTaskWithCallbackBlock:^{
[self hideProgressIndicator];
}];
}
這個例子調用一個方法來顯示指示器,然后創建任務並且告訴他執行。這個回調block指定代碼被執行一旦這個任務完成的時候;這個例子,他只調用一個方法來隱藏指示器。注意這個回調block為了能夠調用隱藏指示器方法當調用時候給self捕獲值。重要的是要照顧好self當他被捕獲的時候,因為可能造成強引用就像在之后描述的這樣:“Avoid Strong Reference Cycles when Capturing self.”
根據代碼的可讀性,這個block可以很容易的看到這個任務完成之前和之后將要發生什么,避免需要追蹤代理方法來發現將要發生什么、
在這個例子中定義這個beginTaskWithCallbackBlock:方法像這樣:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;
這個(void (^)(void))指示這個方法參數block沒有返回值和參數。這個方法實現可以調用block像之前那樣。
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
...
callbackBlock();
}
有一些方法參數希望一個block有一個或者多個參數被指定:
- (void)doSomethingWithBlock:(void (^)(double, double))block {
...
block(21.0, 2.0);
}
一個block應該一直作為方法的最后一個參數
最好的練習是用一個block參數在一個方法里面。如果方法也需要不是block的參數,那么這個block參數應該放在方法的最后一個參數:
- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;
當指定塊為內聯時候更容易被閱讀像這樣:
[self beginTaskWithName:@"MyTask" completion:^{
NSLog(@"The task is complete");
}];
用typedef指令來簡化block語法定義:
如果你需要定義不止一塊具有相同簽名的block,你可能喜歡定義你自己的類型簽名,例如你給一個沒有參數和返回值的block定義一個類型像這樣:
typedef void (^XYZSimpleBlock)(void);
你可以用你的自定義類型作為方法參數或者創建一個block變量:
XYZSimpleBlock anotherBlock = ^{
...
};
- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
...
callbackBlock();
}
自定義類型非常有用當你處理blocks然而這些blocks有blocks的參數或者返回值。考慮下面例子:
void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
...
return ^{
...
};
};
這個complexBlock 變量引用一個blocks作為參數還返回一個block作為返回值.
用自定義類型來重寫代碼讓你的代碼變的可讀性強:
XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
...
return ^{
...
};
};
我們可以給blocks聲明成屬性來跟蹤他:
定義屬性的語法來跟蹤block就像定義block變量:
@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end
注意:我們在定義blocks屬性時候他的關鍵字應該是copy。因為一個block需要被拷貝來跟蹤他捕獲原始范圍以外的狀態。當用ARC的時候我們需要擔心,因為這是自動發生的。但是最好還是給屬性指定關鍵字來表明他的行為。更多信息參考Blocks Programming Topics.
一個block屬性被設置或者引用像其他任何block變量:
self.blockProperty = ^{
...
};
self.blockProperty();
也可以給block屬性重定義像這樣:
typedef void (^XYZSimpleBlock)(void);
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end
當捕獲self的時候小心強引用循環:
如果你需要捕獲self在block中,例如定義一個block回調,你更應該考慮的是他的內存泄漏。
blocks維持着強引用向任何捕獲的對象,包括self,這意味着很容易引起強引用循環,例如一個對象維持着一個copy屬性給一個捕獲self的block
@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
self.block = ^{
[self doSomething]; // capturing a strong reference to self
// creates a strong reference cycle
};
}
...
@end
如果你這樣定義例子編譯器會警告你,但是一個更復雜的例子可能包括多個對象之間的強引用來創建循環,使它更難診斷。
為了避免這個問題,我們聲明一個弱引用類型給self,像這樣:
- (void)configureBlock {
XYZBlockKeeper * __weak weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // capture the weak reference
// to avoid the reference cycle
}
}
給self聲明弱引用類型。這個block沒有維持一個強引用給XYZBlockKeeper對象。如果這個對象在這個block之前被銷毀,這個weakSelf指針將要被指為nil。
blocks可以簡化枚舉
除了一般的完成處理程序,許多的cocoa或者cocoaTouchAPI用blocks來簡化常見的任務,就像集合枚舉。這個NSArray類,例如提供了3個基本的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里的代碼是一個處理器密集的安全並發的,你可以用NSEnumerationConcurrent 選項。
[array enumerateObjectsWithOptions:NSEnumerationConcurrent
usingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
...
}];
這個標識符表明這個枚舉塊可能在多個線程里面調用,如果這個block代碼是特別的密集處理器將提高潛在的性能。注意這個枚舉順序是未定義的當用這個選項的時候:
這個NSDictionay來提供block的基本方法包括:
NSDictionary *dictionary = ...
[dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) {
NSLog(@"key: %@, value: %@", key, obj);
}];
和傳統的循環變量相比他可以很方面的變量字典的鍵值。
塊可以簡化並發任務:
一個block是一個工作單元。組合可執行代碼從周圍范圍捕獲可選狀態。這使異步調用更完美在OSX和IOS用一個有效的並發選項。而不是一定指出怎么使用低級的機制像線程。你可以簡單的定義你的任務使用塊然后讓系統執行這些任務作為處理器資源變的可用。
OSX和IOS提供了很多的並發技術包括兩個任務調度機制: Operation queues and Grand Central Dispatch。這些機制解決的是一個隊列的任務怎么等待被調用。你可以添加blocks到你的隊列順序當你需要他們被調用。並且系統為調用出列當處理器時間和資源可用。
一系列隊列只允許一個任務被執行在同一時間下一個在隊列里面的任務直到前面一個完成才會調用。當前的隊列調用他盡可能調用的任務不用等待前面的任務完成。
使用塊操作和運行隊列:
一個操作隊列在cocoa或者cocoaTouch類似與任務調度。你可以創建一個NSOperation實例來封裝一個工作單元以及必要的數據,然后放到一個NSOperationQueue 里面來執行。
盡管你可以創建一個自定義的NSOperation子類來實現復雜的任務,你也可能用NSBlockOperation 來創建一個操作用block像這樣:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
...
}];
你可能執行一個手動的線程但是這些線程經常被添加一個已經存在的隊列或者你自己創建的隊列,等待被執行:線程
// schedule task on main queue:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
// schedule task on background queue:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
如果你用一個線程隊列,你需要配置優先級或者線程之間的依賴,就像指定一個線程不能被執行直到其他的線程完成。你可以用一個觀察者來觀察線程的狀態。他使你很容易來更新一個進度指示器例如當任務完成了。
更多信息關於線程和線程隊列的參考“Operation Queues”.
在GCD里面用block來調度隊列:
如果你需要安排任意的代碼快來執行,你可以直接用GCD控制的dispatch queues。
隊列調度使他更容易執行同步或者異步並執行他們的任務在一個先進先出的順序。
你也可以創建你自己的隊列調度或者用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()方法立刻返回,無需等待被調用。
dispatch_async(queue, ^{
NSLog(@"Block for asynchronous execution");
});
這個dispatch_sync()方法直到block完成了才能調用。你可以使用他在一種情況下,並發塊需要等待另一個任務在主線程繼續之前完成。
更多信息參考隊列調度和GCD“Dispatch Queues”.
