這個部落格介紹代碼快寫的的不錯, 就是有點多,需要在吃透我前一篇代碼塊一後才能入手這個
學會了代碼塊,ios的編程功力也就上了一個檔次。
轉載部落格: http://blog.sina.com.cn/s/blog_71715bf8010167tl.html
iOS4引入了一個新特性,支援代碼塊的使用,這將從根本上改變你的編程方式。代碼塊是對C語言的一個擴充,因此在Objective-C中完全支援。如果你學過Ruby,Python或Lisp程式設計語言,那麼你肯定知道代碼塊的強大之處。簡單的說,你可以通過代碼塊封裝一組代碼語句並將其當作一個對象。代碼塊的使用是一種新的編碼風格,可以讓你運用自如的使用iOS4中新增API。 我們先來看兩個在iOS4中使用代碼塊的例子(你很有可能已經見過):view animati***** 和enumeration使用代碼塊的例子 第一個例子,假設我們建立一個紙牌遊戲,需要展現紙牌被派發到玩家面前的動畫效果。幸運的是通過UIKit架構可以很容易的實現一個動畫效果。但是最終是什麼樣的動畫是由你的程式決定的。你可以在代碼塊中指定動畫的內容然後再將代碼塊傳給animateWithDuration:animati*****:方法,像下面這樣:[UIView animateWithDuration:2.0 animati*****:^ { 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 animati***** 和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 animati***** 和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 => 3iteration 2 => 6iteration 3 => 9iteration 4 => 12iteration 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 => 3iteration 2 => 6iteration 3 => 9iteration 4 => 12iteration 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 => 3iteration 2 => 6iteration 3 => 18iteration 4 => 72iteration 5 => 360multiplier => 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); // 9block(4); // 16block(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或對電影進行評價等。