iOS4引入了一個新特性,支持代碼塊的使用,這將從根本上改變你的編程方式。代碼塊是對C語言的一個擴展,因此在Objective-C中完全支持。如果你學過Ruby,Python或Lisp編程語言,那麼你肯定知道代碼塊的強大之處。簡單的說,你可以通過代碼塊封裝一組代碼語句並將其當作一個對象。代碼塊的使用是一種新的編碼風格,可以讓你運用自如的使用iOS4中新增API。
我們先來看兩個在iOS4中使用代碼塊的例子(你很有可能已經見過):view animations 和enumeration
使用代碼塊的例子。
第一個例子,假設我們創建一個紙牌游戲,需要展現紙牌被派發到玩家面前的動畫效果。幸運的是通過UIKit框架可以很容易的實現一個動畫效果。但是最終是什麼樣的動畫是由你的程序決定的。你可以在代碼塊中指定動畫的內容然後再將代碼塊傳給animateWithDuration:animations:方法,像下面這樣:
[UIView animateWithDuration:2.0
animations:^ {
self.cardView.alpha = 1.0;
self.cardView.frame = CGRectMake(176.0, 258.0, 72.0, 96.0);
self.cardView.transform = CGAffineTransformMakeRotation(M_PI);
}
];
當這個動畫代碼塊執行時,我們的紙牌會展現三種方式的動畫:改變它的alpha值從而淡入顯示,改變它的位置到右下角(玩家的位置),以及自轉180度(為了使其效果更好)。
第二個代碼塊的例子是迭代一個紙牌的集合,並打印其名字和在集合裡的索引值。
你可以通過使用for循環來達到目的,但是在iOS4中NSArray類有一個使用了代碼塊的方便方法:enumerateObjectsUsingBlock:。下面是如何使用它:
NSArray *cards = [NSArray arrayWithObjects:@"Jack", @"Queen", @"King", @"Ace", nil];
[cards enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
NSLog(@"%@ card at index %d", object, index);
}];
這個代碼塊使用了三個參數:數組中的一個對象,該對象的索引,以及一個標識迭代是否結束的標志。我們稍候再對其進一步探討。enumerateObjectsUsingBlock: 這個方法會將集合中的每一個元素傳入相應的參數並調用代碼塊中的方法。
因此在你的Mac和IOS程序中使用代碼塊的優勢是:它允許你附加任意的代碼到蘋果官方提供的方法上。盡管在概念上與代理相似,但是在方法中使用簡短的內聯代碼塊往往更加方便,更加優雅。
這是一個好的開始,但重要的是要明白它內部的處理。當我學習新東西的時候,我喜歡先將其分為一個個簡單的部分,了解它們如何工作,然後再將它們組裝到一塊,這樣我會對自己寫的代碼以及快速解決出現的問題充滿信心。因此,讓我們先回頭學習下如何聲明和調用簡單的代碼塊。
代碼塊的基本概念
一個代碼塊可以簡單看作是一組可執行的代碼。例如,下面是一個打印當前日期和時間的代碼塊:
^ {
NSDate *date = [NSDate date];
NSLog(@"The date and time is %@", date);
};
插 入符號(^)聲明一個代碼塊的開始,一對大括號{}構成了代碼塊的體部。你可以認為代碼塊與一個匿名函數類似。那麼,如果是一個匿名的函數,我們該怎麼調用這個代碼塊呢?最常見使用代碼塊的方式是將其傳入方法中供方法回調,就像之前我們已經見到了view animations 和enumeration。另一種使用代碼塊的方式是將其賦予代碼塊變量,然後可使用該變量來直接調用代碼塊。以下是如何聲明我們的代碼塊並將它賦予代碼塊變量now:
void (^now)(void) = ^ {
NSDate *date = [NSDate date];
NSLog(@"The date and time is %@", date);
};
聲明一個塊變量的語法需要一些時間適應,這才有趣。如果你使用過函數指針,代碼塊變量與其類似。在上面代碼等號右邊是我們已經介紹過的代碼塊。等號左邊我們聲明了一個代碼塊變量now。
代碼塊變量之前有^符號並被小括號包著,代碼塊變量有類型定義的。因此,上圖中的now變量可以應用任何無參,無返回值的代碼塊。我們之前聲明的代碼塊符合這要求,,所以我們可以放心的把它分配給now變量。
只要有一個代碼塊變量,並在其作用域范圍內,我們就可以像調用函數一樣來調用它。下面是如何調用我們的代碼塊:
now();
你可以在C函數或者Objective-c方法中聲明代碼塊變量,然後在同一作用域內調用它,就像我們前面說明那樣。當代碼塊執行時,它打印當前的日期和時間。目前為止,進展順利。
代碼塊是閉包
如果這就是代碼塊的全部的話,那麼他與函數是完全相同的。但事實是代碼塊不僅僅是一組可執行的代碼。代碼塊能夠捕捉到已聲明的同一作用域內的變量,同時由於代碼塊是閉包,在代碼塊聲明時就將使用的變量包含到了代碼塊范圍內。為了說明這一點,讓我們改變一下前面的例子,將日期的初始化移到代碼塊之外。
NSDate *date = [NSDate date];
void (^now)(void) = ^ {
NSLog(@"The date and time is %@", date);
};
now();
當你第一次調用這個代碼塊的時候,它與我們之前的版本結果完全一致:打印當前的日期和時間。但是當我們改變日期後再調用代碼塊,那麼就會有顯著的不同了;
sleep(5);
date = [NSDate date];
now();
盡管我們在調用代碼塊之前改變了日期,但是當代碼塊調用時仍然打印的是之前的日期和時間。就像是日期在代碼塊聲明時停頓了一樣。為什麼會這樣呢,當程序執行到代碼塊的聲明時,代碼塊對同一作用域並且塊內用到的變量做一個只讀的備份。你可以認為變量在代碼塊內被凍結了。因此,不論何時當代碼塊被調用時,立即調用或5秒鐘之後,只要在程序退出之前,它都是打印最初的日期和時間。
事實上,上面那個展示代碼塊是閉包的例子並不十分完善,畢竟,你可以將日期作為一個參數傳入到代碼塊中(下面講解)。但是當你將代碼塊在不同方法間傳遞時閉包的特性就會變得十分有用,因為它裡面的變量是保持不變的。
代碼塊參數
就像函數一樣,代碼塊可以傳入參數和返回結果。例如,我們想要一個能夠返回指定數的三倍的代碼塊,下面是實現的代碼塊:
^(int number) {
return number * 3;
};
為代碼塊聲明一個變量triple,如下:
int (^triple)(int) = ^(int number) {
return number * 3;
};
上面說過,我們需要熟悉等號左邊聲明代碼塊變量的語法。現在讓我們從左到右分開來說明:
最左邊的int是返回值類型,中間是小括號包圍插入符號^及代碼塊變量的名字,最後又一個小括號,包圍著參數的類型(上面例子中只有一個int參數)。等號右邊的代碼塊聲明必須符合左側的定義。有一點要說明的是,為了方便,可以不聲明代碼塊的返回類型,編譯器會從返回語句中做出判斷。
要調用這個代碼塊,你需要傳入一個需要乘3的參數,並接受返回值,像這樣:
int result = triple(2);
下面你將知道如何聲明並創建一個需要兩個int型參數,將它們相乘然後返回結果的代碼塊:
int (^multiply)(int, int) = ^(int x, int y) {
return x * y;
};
這是如何調用這個代碼塊:
int result = multiply(2, 3);
聲明代碼塊變量使我們有機會探討代碼塊類型以及如何調用。代碼塊變量類似函數指針,調用代碼塊與調用函數相似。不同於函數指針的是,代碼塊實際上是Objective-C對象,這意味著我們可以像對象一樣傳遞它們。
調用代碼塊的方法
在實際中,代碼塊經常被作為參數傳入方法中供其回調。當把代碼塊作為一個參數時,相比分配一個代碼塊變量,更通常的做法是作為內聯代碼塊。例如,我們之前看到的例子:view animations 和enumeration。
蘋果官方已經增加了一些使用代碼塊的方法到他們的框架中。你也可以寫一些使用代碼塊的API了。例如,我們要創建一個Worker類的使用代碼塊的類方法,該方法重復調用代碼塊指定的次數,並處理代碼塊每次返回的結果。下面是我們使用內聯代碼塊調用這個方法,代碼塊負責返回1到10的每個數的三倍。
[Worker repeat:10 withBlock:^(int number) {
return number * 3;
}];
這個方法可以將任何接受一個int型參數並返回一個int型結果的代碼塊作為參數,如果想得到數字的二倍,只需要改變傳入方法的代碼塊。
編寫使用代碼塊的方法
在第一部分我們留下了一個任務:寫一個Work類的調用代碼塊的類方法,並且重復調用代碼塊指定的次數,還要處理每次代碼塊的返回值。如果我們想要得到1到5的三倍的話,那麼下面是我們該如何調這個帶有內聯代碼塊的方法:
[Worker repeat:5 withBlock:^(int number) {
return number * 3;
}];
我經常這樣設計一個類,首先寫代碼調用一個虛構的方法,這也是在提交之前一種形成API的簡單方式,一旦認為這個方法調用正確,我就去實現這個方法。這樣, 那個方法的名字是repeat:withBlock:,我認為不合適(我知道在第一部分是叫這個名字,但我已經改變注意了)。這個名字容易使人混淆,因為該方法實際上並不是重復做相同的事情。這個方法從1迭代到指定的次數,並處理代碼塊的返回。所以讓我們開始正確的重命名它:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
return number * 3;
}];
我對這個使用兩個參數的方法的名字iterateFromOneTo:withBlock:很滿意,一個int型參數表示調用代碼塊的次數和一個要被調用的代碼塊參數。現在讓我們去實現這個方法。
對於初學者,我們該如何聲明這個 iterateFromOneTo:withBlock:方法呢?首先我們需要知道所有參數的類型,第一個參數很容易,是個int類型;第二個參數是一個代碼塊,代碼塊是有返回類型的。在這個例子中,這個方法可以接受任何有一個int型參數並返回int型結果的代碼塊作為參數。下面是實際的代碼塊類型:
int (^)(int)
已經有了方法的名字和它的參數類型,我們就可以聲明這個方法了。這是Worker類的類方法,我們在worker.h中聲明它:
@interface Worker : NSObject {
}
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;
@end
第一眼看去,代碼塊參數不容易理解。有個要記住訣竅是:在Objective-C中所有的方法參數有兩個部分組成。被括起來的參數類型以及參數的名稱。這個例子中,參數的要求是一個是int型和一個是int(^)(int)型的代碼塊(你可以為參數命名為任意的名字,不一定非得是block)。這個方法的實現是在Worker.m文件文件中,比較簡單:
#import "Worker.h"
@implementation Worker
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block {
for (int i = 1; i <= limit; i++) {
int result = block(i);
NSLog(@"iteration %d => %d", i, result);
}
}
@end
方法通過一個循環來每次調用代碼塊,並打印出代碼塊的返回結果。記住一旦我們在作用域內有一個代碼塊變量,那麼就可以像函數一樣使用它。在這裡代碼塊參數就是一個代碼塊變量。因此,當執行block(i)時就會調用傳入的代碼塊。當代碼塊返回結果後會繼續往下執行。現在我們可以使用內聯代碼塊的方式調用 iterateFromOneTo:withBlock:方法,像這樣:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
return number * 3;
}];
我們也可以不使用內聯代碼塊的方式,傳入一個代碼塊變量作為參數:
int (^tripler)(int) = ^(int number) {
return number * 3;
};
[Worker iterateFromOneTo:5 withBlock:tripler];
不論那種方式,我們得到的輸出如下:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15
當然我們可以傳入進行任何運算的代碼塊。想要得到數字的平方嗎?沒問題,只要傳入一個不同的代碼塊:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
return number * number;
}];
現在我們的代碼是可以運行的,下面將代碼稍微整理下吧。
善於使用Typedef
匆忙的聲明代碼塊的類型容易混亂,即使在這個簡單的例子中,函數指正的語法還是有許多不足之處:
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;
試想代碼塊要使用多個參數,並且有些參數是指針類型,這樣的話你幾乎需要完全重寫你的代碼。為了提高可讀性和避免在.h和.m中出項重復,我們可以使用typedef修改Worker.h文件:
typedef int (^ComputationBlock)(int);
@interface Worker : NSObject {
}
+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block;
@end
typedef 是C語言的一個關鍵字,其作用可以理解為將一個繁瑣的名字起了一個昵稱。在這種情況下,我們定義一個代碼塊變量ComputationBlock,它有一個int型參數和一個int型返回值。然後,我們定義iterateFromOneTo:withBlock:方法時,可以直接使用 ComputationBlock作為代碼塊參數。同樣,在Worker.m文件,我們可以通過使用ComputationBlock簡化代碼:
#import "Worker.h"
@implementation Worker
+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block {
for (int i = 1; i <= limit; i++) {
int result = block(i);
NSLog(@"iteration %d => %d", i, result);
}
}
@end
嗯, 這樣就好多了,代碼易於閱讀,沒有在多個文件重復定義代碼塊類型。事實上,你可以使用ComputationBlock在你程序的任何地方,只要 import “Worker.h”,你會碰到類似的typedef在新的iOS4的API中。例如,ALAssetsLibrary類定義了下面的方法:
- (void)assetForURL:(NSURL *)assetURL
resultBlock:(ALAssetsLibraryAssetForURLResultBlock)resultBlock
failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock
這個方法調用兩個代碼塊,一個代碼塊時找到所需的資源時調用,另一個時沒找到時調用。它們 的 typedef如下:
typedef void (^ALAssetsLibraryAssetForURLResultBlock)(ALAsset *asset);
typedef void (^ALAssetsLibraryAccessFailureBlock)(NSError *error);
然後在你的程序中可以使用ALAssetsLibraryAssetForURLResultBlock和ALAssetsLibraryAccessFailureBlock去表示相應的代碼塊變量。
我建議在寫一個使用代碼塊的公用方法時就用typedef,這樣有助於你的代碼整潔,並可以讓其他開發人員方便使用。
再來看一下閉包
你應該還記得代碼塊是閉包,我們簡要的講述一下在第一部分提及的閉包。在第一部分閉包的例子並不實用,而且我說閉包在方法間傳遞時會變得特別有用。現在我們已經知道如何寫一個實用代碼塊的方法,那麼就讓我們分析下另一個閉包的例子:
int multiplier = 3;
[Worker iterateFromOneTo:5 withBlock:^(int number) {
return number * multiplier;
}];
我們使用之前寫的iterateFromOneTo:withBlock:方法,有一點不同的是沒有將要得到的倍數硬編碼到代碼塊中,這個倍數被聲明在代碼塊之外,為一個本地變量。該方法執行的結果與之前一致,將1到5之間的數乘3:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15
這個代碼的運行是一個說明閉包強大的例子。代碼打破了一般的作用域規則。實際上,在iteratefromOneTo:withBlock:方法中調用multiplier變量,可以把它看作是本地變量。
記住,代碼塊會捕捉周圍的狀態。當一個代碼塊聲明時它會自動的對其內部用到的變量做一個只讀的快照。因為我們的代碼塊使用了multiplier變量,這個變量的值被代碼塊保存了一份供之後使用。也就是說,multiplier變量已經成為了代碼塊狀態啊的一部分。當代碼塊被傳入到 iterateFromOneTo:withBlock:方法,快的狀態也傳了進去。
好吧,如果我們想在代碼塊的內部改變multiplier變量該怎麼辦?例如,代碼塊每次被調用時要讓multiplier變為上一次計算的結果。你可能會試著在代碼塊裡直接改變multiplier變量,像這樣:
int multiplier = 3;
[Worker iterateFromOneTo:5 withBlock:^(int number) {
multiplier = number * multiplier;
return multiplier; // compile error!
}];
這樣的話是通不過編譯的,編譯器會報錯“Assignment of read-only variable 'mutilplier'”。這是因為代碼塊內使用的是變量的副本,它是堆棧裡的一個常量。這些變量在代碼塊中是不可改變的。
如果你想要修改一個在塊外面定義,在塊內使用的變量時,你需要在變量聲明時增加新的前綴_block,像這樣:
__block int multiplier = 3;
[Worker iterateFromOneTo:5 withBlock:^(int number) {
multiplier = number * multiplier;
return multiplier;
}];
NSLog(@"multiplier => %d", multiplier);
這樣代碼可以通過編譯,運行結果如下:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 18
iteration 4 => 72
iteration 5 => 360
multiplier => 360
要 注意的是代碼塊運行之後,multiplier變量的值已經變為了360。換句話說,代碼塊內部修改的不是變量的副本。聲明一個被_block修飾的變量 是將其引用傳入到了代碼塊內。事實上,被_block修飾的變量是被所有使用它的代碼塊共享的。這裡要強調的一點是:_block不要隨便使用。在將一些東西移入內存堆中會存在邊際成本,除非你真的確定需要修改變量,否則不要用_block修飾符。
編寫返回代碼塊的方法
有時我們會需要編寫一個返回代碼塊的方法。讓我先看一個錯誤的例子:
+ (ComputationBlock)raisedToPower:(int)y {
ComputationBlock block = ^(int x) {
return (int)pow(x, y);
};
return block; // Don't do this!
}
這種方法簡單的創建了一個計算y的x次冪的代碼塊然後返回它。它使用了我們之前通過typedef使用的ComputationBlock。下面是我們對所返回代碼塊的期望效果:
ComputationBlock block = [Worker raisedToPower:2];
block(3); // 9
block(4); // 16
block(5); // 25
在上面的例子中,我們使用的得到代碼塊,傳入相應的參數,它應該會返回傳入值的平方。但是當我們運行它時,會得到運行時錯誤”EXC_BAD_ACCESS”。
怎麼辦?解決這個問題的關鍵是了解代碼塊是怎麼分配內存的。代碼塊的生命周期是在棧中開始的,因為在棧中分配內存是比較快的。是棧變量也就意味著它從棧中彈出後就會被銷毀。方法返回結果就會發生這樣的情況。
回顧我們的raisedToPower:方法,可以看到在方法中創建了代碼塊並將它返回。這樣創建代碼塊就是已明確代碼塊的生存周期了,當我們返回代碼塊變量後,代碼塊其實在內存中已經被銷毀了。解決辦法是在返回之前將代碼塊從棧中移到堆中。這聽起來很復雜,但是實際很簡單,只需要簡單的對代碼塊進行copy 操作,代碼塊就會移到堆中。下面是修改後的方法,它可以滿足我們的預期:
+ (ComputationBlock)raisedToPower:(int)y {
ComputationBlock block = ^(int x) {
return (int)pow(x, y);
};
return [[block copy] autorelease];
}
注 意我們使用了copy後就必須跟一個autorelease從而平衡它的引用計數器,避免內存洩露。當然我們也可以在使用代碼塊之後將其手動釋放,不過這就不符合誰創建誰釋放的原則了。你不會經常需要對代碼塊進行copy操作,但是如果是上面所講的情況你就需要了,這點請留意。
將所學的整合在一起;
那麼,讓我們來把所學的東西整合為一個更實際點的例子。假設我們要設計一個簡單的播放電影的類,這個類的使用者希望電影播放完之後能夠接受一個用於展現應用特定邏輯的回調。前面已經證明代碼塊是處理回調很方便的方法。
讓我們開始寫代碼吧,從一個使用這個類的開發人員的角度來寫:
MoviePlayer *player =
[[MoviePlayer alloc] initWithCallback:^(NSString *title) {
NSLog(@"Hope you enjoyed %@", title);
}];
[player playMovie:@"Inception"];
可以看出我們需要MoviePlayer類,他有兩個方法:initWithCallback:和playMovie:,初始化的時候接受一個代碼塊,然後將它保存起來,在執行playMovie:方法結束後再調用代碼塊。這個代碼塊需要一個參數(電影的名字),返回void類型。我們對回調的代碼塊類型使用typedef,使用property來保存代碼塊變量。記住,代碼塊是對象,你可以像實例變量或屬性一樣使用它。這裡我們將它當作屬性使用。下面是 MoviePlayer.h:
typedef void (^MoviePlayerCallbackBlock)(NSString *);
@interface MoviePlayer : NSObject {
}
@property (nonatomic, copy) MoviePlayerCallbackBlock callbackBlock;
- (id)initWithCallback:(MoviePlayerCallbackBlock)block;
- (void)playMovie:(NSString *)title;
@end
下面是MoviePlayer.m:
#import "MoviePlayer.h"
@implementation MoviePlayer
@synthesize callbackBlock;
- (id)initWithCallback:(MoviePlayerCallbackBlock)block {
if (self = [super init]) {
self.callbackBlock = block;
}
return self;
}
- (void)playMovie:(NSString *)title {
// play the movie
self.callbackBlock(title);
}
- (void)dealloc {
[callbackBlock release];
[super dealloc];
}
@end
在 initWithCallback:方法中將要使用的代碼塊聲明為callbackBlock屬性。由於屬性被聲明為了copy方式,代碼塊會自動進行 copy操作,從而將其移到堆中。當playMovie:方法調用時,我們傳入電影的名字作為參數來調用代碼塊。
現在我們假設一個開發人員要在程序中使用我們的MoviePlayer類來管理一組你打算觀看的電影。當你看完一部電影之後,這部電影就會從組中移除。下面是一個簡單的實現,使用了閉包:
NSMutableArray *movieQueue =
[NSMutableArray arrayWithObjects:@"Inception",
@"The Book of Eli",
@"Iron Man 2",
nil];
MoviePlayer *player =
[[MoviePlayer alloc] initWithCallback:^(NSString *title) {
[movieQueue removeObject:title];
}];
for (NSString *title in [NSArray arrayWithArray:movieQueue]) {
[player playMovie:title];
};
請注意代碼塊使用了本地變量movieQueue,它會成為代碼塊狀態的一部分。當代碼塊被調用,就會從數組movieQueue中移除一個電影,盡管此時 數組是在代碼塊作用域之外的。當所有的電影播放完成之後,movieQueue將會是一個空數組。下面是一些需要提及的重要事情:
1、movieQueue變量是一個數組指針,我們不能修改它的指向。我們修改的是它指向的內容,因此不需要使用_block修飾。
2、為了迭代movieQueue數組,我們需要創建一個它的copy,否則如果我們直接使用movieQueue數組,就會出現在迭代數組的同事還在移除它的元素,這會引起異常。
3、如果不使用代碼塊,我們可以聲明一個協議,寫一個代理類,並注冊這個代理作為回調。很明顯該例子使用內聯代碼塊更方便。
4、在不改變MoviePlayer類的前提下可以給他增加新功能。比如另一個開發者可以在看完一部電影後將其分享到twitter或對電影進行評價等。