本文檔版權歸NickTang所有,沒有本人書面或電子郵件允許,不許轉載,摘錄,發表。多謝!
一段時間以來,塊代碼已經成爲Ruby,Python,Lisp等腳本語言和編譯語言中的一部分(在這些語言中,可能被命名爲“closures”或“lambdas”)。從Mac OS X v10.6和iOS 4.0開始,塊代碼,一個強大的C語言功能點,已經是Cocoa應用開發的一部分了。雖它的語法初看起來有點奇怪,但是你會發現它是很好用的。
下面的討論都是大概的描述,如果你希望非常詳細,定義性的解釋,請參看Blocks Programming Topics。
爲何使用塊代碼?
塊代碼是一個能工作的代碼單元,可以在任何時候被執行。它們本質上是輕量級的,匿名的函數,並且可以作爲函數的參數,或者返回值。塊代碼本身可能有一個參數列表,返回類型,或者沒有返回值。你可以把塊代碼賦給一個變量,並在合適的時候調用它,就行調用一個普通函數一樣。
插入符(^)被用來做爲塊代碼的開始標記。例如,下面的代碼就聲明瞭一個塊塊代碼,它有兩個整形的參數,返回類型也是整形。這裏在第一個插入符後面列出參數列表,在一對大括號中包含實現代碼,並且把這個快代碼賦值給變量Multiply
:
int (^Multiply)(int, int) = ^(int num1, int num2) { |
return num1 * num2; |
}; |
int result = Multiply(7, 4); // result is 28 |
作爲函數或者方法的參數的時候,塊代碼其實就是一個回調類型的函數,可以使用在侷限於函數或方法類型的代理上。作爲參數傳入後,在調用塊代碼,可以使得函數和方法可以實現個性化運行。當調用這些函數或方法的時候,它們會在合適的時候,執行這些塊代碼,去取的附加信息,或者執行特定的行爲。
使用塊代碼作爲函數或方法的參數的一個好處就是,你可以在調用函數或方法的地方寫回調代碼。由於這些代碼不需要在額外的函數或方法中,所以這樣的實現方式簡單易懂。使用通知中類NSNotification
作爲一個例子。在過去的模式下,一個對象把自己加入到一個通知的觀察者對象中,它需要實現一個額外的函數(在調用addObserver:..
函數的時候使用選擇器作爲參數傳入)去處理這個消息:
- (void)viewDidLoad { |
[super viewDidLoad]; |
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(keyboardWillShow:) |
name:UIKeyboardWillShowNotification object:nil]; |
} |
|
- (void)keyboardWillShow:(NSNotification *)notification { |
// Notification-handling code goes here. |
} |
在新的函數addObserverForName:object:queue:usingBlock:
中,你可以替換通知的回調函數爲下面的形式:
- (void)viewDidLoad { |
[super viewDidLoad]; |
[[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification |
object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { |
// Notification-handling code goes here. |
}]; |
} |
使用塊代碼的另一個好處是,塊代碼可以共享本地作用域內有效的變量。如果你在一個函數中實現了一個塊代碼,這個塊代碼可以使用本地變量和函數參數(即棧上的變量),還有在函數中能用包含實例變量等的全局變量。塊代碼在使用上述變量的時候是隻讀的,即不能修改這些變量,不過如果你使用__block來修飾變量,那麼這個變量對於塊代碼就是可寫的。即使方法或函數已經退出,本地環境已經釋放,這些在塊代碼中使用的變量還存在,只要還有對這個塊代碼的引用存在。
系統框架API中的塊代碼
一個顯著的使用塊代碼的動機是在系統類庫中的函數越來越多的開始使用這個作爲參數,下面是在部分系統函數中使用塊代碼的情況:
-
結束回調
-
通知處理
-
出錯處理
-
枚舉
-
視圖動畫和翻轉
-
排序
下面各節就是對上面的討論。不過在開始討論之前,我們還需要看一下系統函數中的塊代碼的聲明。考慮下面的這個在類NSSet
中的聲明:
- (NSSet *)objectsPassingTest:(BOOL (^)(id obj, BOOL *stop))predicate |
上面的這個聲明標示着傳入函數的參數是一個動態對象類型和一個布爾類型,返回一個布爾類型的代碼塊。(傳入參數和返回類型都是用在“Enumeration” 中的for循環的類型)。當聲明你的塊代碼的時候,使用一個插入符^開始,並且使用一對括號包起來的參數列表,後面跟着被一對大括號包着的代碼:
[mySet objectsPassingTest:^(id obj, BOOL *stop) { |
// Code goes here; end by returning YES or NO. |
}]; |
結束回調和錯誤管理
結束處理是一個回調函數,用在當一個調用端使用系統函數或方法完成一個任務後的處理。很多時候調用端在結束回調中實現釋放狀態或者更新界面。很多框架方法函數使用塊代碼作爲結束回調。
UIView
類中有很多個實現動畫或視圖翻轉的函數使用塊代碼作爲結束回調參數。(“View
Animation and Transitions”對這些函數做出了描述)代碼1-1演示瞭如何使用animateWithDuration:animations:completion:
函數的例子。這個例子中的動畫結束回調,在結束後使得做動畫的視圖回覆原位,並且幾秒後把alpha設置爲1。
一個結束回調塊代碼
- (IBAction)animateView:(id)sender { CGRect cacheFrame = self.imageView.frame; [UIView animateWithDuration:1.5 animations:^{ CGRect newFrame = self.imageView.frame; newFrame.origin.y = newFrame.origin.y + 150.0; self.imageView.frame = newFrame; self.imageView.alpha = 0.2; } completion:^ (BOOL finished) { if (finished) { // Revert image view to original. sleep(3); self.imageView.frame = cacheFrame; self.imageView.alpha = 1.0; } }]; } |
一些框架函數具有出錯處理,這個和結束處理是一樣的。函數會在默寫錯誤發生的時候不能完成任務的情況下調用出錯處理(並且傳入一個NSError
對象)。你可以自定義這個出錯處理來通知用戶有錯誤發生。
通知管理
NSNotificationCenter類中的方法
addObserverForName:object:queue:usingBlock:讓你在設置一個通知的觀察者的時候就可以實現一個處理代碼塊。代碼
1-2演示了調用這個函數的情況,爲某個通知定義一個通知管理的代碼塊。作爲一個通知管理的函數,一個NSNotification
對象被傳入。這個函數還使用了一個NSOperationQueue
實例,你的應用可以使用它來傳遞執行上下文到代碼塊中。
Adding an object as an observer and handling a notification using a block
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { |
opQ = [[NSOperationQueue alloc] init]; |
[[NSNotificationCenter defaultCenter] addObserverForName:@"CustomOperationCompleted" |
object:nil queue:opQ |
usingBlock:^(NSNotification *notif) { |
NSNumber *theNum = [notif.userInfo objectForKey:@"NumberOfItemsProcessed"]; |
NSLog(@"Number of items processed: %i", [theNum intValue]); |
}]; |
} |
枚舉
基礎類庫中的容器類—NSArray
,NSDictionary
,NSSet
,和NSIndexSet
—定義的可以實現枚舉的函數,使用塊代碼作爲參數,以便調用者可以個性化枚舉動作。換句話說,這些方法提供了一個快速枚舉的環境:
for (id item in collection) { |
// Code to operate on each item in turn. |
} |
有兩種類型枚舉函數使用塊代碼。第一種是函數名稱以enumerate開頭並且沒有返回值,這些函數使用塊代碼對沒有被枚舉的對象進行處理;第二種是以
passingTest
;結束的函數,這樣函數返回一個整形或者NSIndexSet對象,這類函數中的代碼塊對每一個枚舉對象進行測試,如果通過測試返回YES。函數返回的整形或者索引表示通過測試的原始位置或所有通過測試的對象的位置。
代碼1-3中對三個中沒一個都調用NSArray中的方法。第一個方法(一個“passing test”方法)的塊代碼在數組中的每一個字符串對象如果含有一個固定的前綴就返回YES。後面的代碼使用方法返回的索引創建一個臨時的數組。第二個塊代碼把每一個第一個數組中的前綴去掉,把後面的加入到一個新數組中。
使用兩個塊代碼枚舉數組
NSString *area = @"Europe"; |
NSArray *timeZoneNames = [NSTimeZone knownTimeZoneNames]; |
NSMutableArray *areaArray = [NSMutableArray arrayWithCapacity:1]; |
NSIndexSet *areaIndexes = [timeZoneNames indexesOfObjectsWithOptions:NSEnumerationConcurrent |
passingTest:^(id obj, NSUInteger idx, BOOL *stop) { |
NSString *tmpStr = (NSString *)obj; |
return [tmpStr hasPrefix:area]; |
}]; |
|
NSArray *tmpArray = [timeZoneNames objectsAtIndexes:areaIndexes]; |
[tmpArray enumerateObjectsWithOptions:NSEnumerationConcurrent|NSEnumerationReverse |
usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { |
[areaArray addObject:[obj substringFromIndex:[area length]+1]]; |
}]; |
NSLog(@"Cities in %@ time zone:%@", area, areaArray); |
上面的每一個枚舉函數中的stop參數(這裏沒有使用)可以用來傳入一個YES,在合適的時候停止枚舉。你可以使用它在枚舉到第一個你需要的項的時候停止。
NSString類,儘管不是一個容器類,也提供兩個使用塊代碼的函數,它們分別是
enumerateSubstringsInRange:options:usingBlock:
和enumerateLinesUsingBlock:。第一個函數枚舉一個字符串,使用一個子串進行分割,第二個只是使用換行符進行分割。代碼
1-4演示了第一個函數如何使用:
使用塊代碼在一個字符串中查找匹配的子串
NSString *musician = @"Beatles"; |
NSString *musicDates = [NSString stringWithContentsOfFile: |
@"/usr/share/calendar/calendar.music" |
encoding:NSASCIIStringEncoding error:NULL]; |
[musicDates enumerateSubstringsInRange:NSMakeRange(0, [musicDates length]-1) |
options:NSStringEnumerationByLines |
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { |
NSRange found = [substring rangeOfString:musician]; |
if (found.location != NSNotFound) { |
NSLog(@"%@", substring); |
} |
}]; |
視圖動畫和轉換
iOS4.0中的UIView
類引入幾個使用塊代碼,實現動畫和轉換的類方法,這些塊代碼類型的參數有兩種(不是所有的函數都使用兩個類型):
-
塊代碼用來改變視圖的屬性來形成動畫
-
動畫完成後的管理
代碼1-5演示了使用animateWithDuration:animations:completion:
的使用,這個函數使用了這兩種塊代碼參數。在這個例子中,動畫使得視圖消失(設置alpha值爲0)並在動畫完成後把視圖移除。
視圖使用塊代碼實現簡單動畫
[UIView animateWithDuration:0.2 animations:^{ |
view.alpha = 0.0; |
} completion:^(BOOL finished){ |
[view removeFromSuperview]; |
}]; |
UIView中其他的一些類方法實現了兩個不同視圖的轉換,包括翻轉和盤旋。下面的例子代碼就是使用transitionWithView:duration:options:animations:completion:
做一個從左面開始的翻轉(代碼中沒有實現動畫完成的處理):
在兩個視圖間做翻轉
[UIView transitionWithView:containerView duration:0.2 |
options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{ |
[fromView removeFromSuperview]; |
[containerView addSubview:toView] |
} |
completion:NULL]; |
排序
基礎類庫中聲明瞭NSComparator塊代碼類型來進行兩個條目的比較:
typedef NSComparisonResult (^NSComparator)(id obj1, id obj2); |
NSComparator是一個塊代碼類型,並且使用兩個對象作爲參數,返回一個
NSComparisonResult類型的值。它是
NSSortDescriptor
,NSArray
,
和NSDictionary等類的方法的參數,用來進行排序,如下所示:
使用NSComparator塊代碼功能對數組進行排序
NSArray *stringsArray = [NSArray arrayWithObjects: |
@"string 1", |
@"String 21", |
@"string 12", |
@"String 11", |
@"String 02", nil]; |
static NSStringCompareOptions comparisonOptions = NSCaseInsensitiveSearch | NSNumericSearch | |
NSWidthInsensitiveSearch | NSForcedOrderingSearch; |
NSLocale *currentLocale = [NSLocale currentLocale]; |
NSComparator finderSort = ^(id string1, id string2) { |
NSRange string1Range = NSMakeRange(0, [string1 length]); |
return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale]; |
}; |
NSLog(@"finderSort: %@", [stringsArray sortedArrayUsingComparator:finderSort]); |
上面的代碼是從Blocks Programming Topics裏面選取的。
塊代碼和併發
塊代碼是一個匿名對象,適合於異步調用,由於上面的這些特徵,塊代碼和適合GCD(Grand Central Dispatch (GCD))和NSOperationQueue
,
-
GCD中兩個關鍵函數
dispatch_sync
(同步派發)和dispatch_async
(異步派發)的第二個參數都是一個塊代碼類型。
-
一個NSOperationQueue是一個用來調度任務的對象,這些任務可能順序執行,或者有相互依賴關係。這些任務其實是
NSOperation
對象,這些對象一般使用塊代碼來實現任務。
NSOperationQueue和
NSOperation的更多信息,請參看
Concurrency
Programming Guide。