Working with Blocks - -Block的使用

在編程領域里,一個牛逼程序員和一個二逼程序員之間的區(qū)別主要是其對所用編程語言優(yōu)秀特性的運用方式。要說到Objective-C語言時,那么一般開發(fā)者和大牛的區(qū)別可能就是對Block書寫代碼的運用能力了。

Block編程并不是Objective-C語言獨創(chuàng)的一個編程方式,Block也同時也以其他的命名方式存在于其他的編程語言中,例如在Javascript中閉包;Block首次于iOS 4.0版本中引入,其后便被廣泛地接受和運用。在隨后的iOS版本中,為了適用Block,Apple重寫了很多的framework方法。似乎Block在一定程度上已經(jīng)成為了未來的一種編程方式。但是Block到底是什么呢?

Block是什么

Block是一種添加到C、Objective-C和C++語言中的一個語言層面的特性,它允許您創(chuàng)建不同的代碼段,并像值一樣的傳遞到方法或函數(shù)中。Block是一個Objective-C對象,這就意味著其可以被保存在NSArray或者NSDictionary中,Block還能夠在自己的封閉作用域中截獲到值(即所謂的變量截獲),Block其實和其他編程語言中的closure(閉包)或lambda是很類似的。

Block 語法

blocks.jpg

在定義Block的語法中我們使用脫字符(^)來標識這是一個Block,如下所示:

    ^{
         NSLog(@"This is a block");
    }

與函數(shù)和方法定義一樣,大括號同時也代表著Block的開始與結束。 在這個例子中,Block不返回任何值,并且不接受任何參數(shù)。

與通過使用函數(shù)指針來引用C函數(shù)的類似方式,你也可以通過聲明一個變量來記錄Block,如:

void (^simpleBlock)(void);

如果你對處理C語言的函數(shù)指針不熟悉,那么上面的這種語法看起來會有點讓人摸不著頭腦。 上面的例子中聲明了一個名字為simpleBlock的變量,用以引用一個沒有參數(shù)也沒有返回值的Block,這意味著這個Block變量可以被最上面的Block所賦值,如下所示:

    simpleBlock = ^{
        NSLog(@"This is a block");
    };

這和任何其他變量賦值一樣,所以語法上必須以大括號后面的分號作為結束。 您也可以將Block變量的聲明和賦值組合起來:

    void (^simpleBlock)(void) = ^{
        NSLog(@"This is a block");
    };

一旦Block被聲明且賦值后,您就可以調用Block了,調用方法如下:

simpleBlock();

注意:如果你試圖調用一個沒有被賦值過的Block變量,你的應用會崩潰的。

Block的參數(shù)和返回值

像方法和函數(shù)一樣,Block即接受參數(shù)也有返回值;例如,一個返回兩個值乘積的Block變量:

double (^multiplyTwoValues)(double, double);

對應于上面的Block變量,其相應的Block應該是這樣的:

    ^ (double firstValue, double secondValue) {
        return firstValue * secondValue;
    }

firstValue和secondValue用于引用在調用Block時提供的值,就像任何函數(shù)定義一樣。 在此示例中,返回類型是從Block內的return語句推斷的。

如果你喜歡,你可以通過在脫字符(^)和參數(shù)列表之間指定來使返回類型顯式地寫出:

    ^ double (double firstValue, double secondValue) {
        return firstValue * secondValue;
    }

一旦你聲明和定義了Block,你就可以像調用函數(shù)那樣調用Block:

    double (^multiplyTwoValues)(double, double) =
                              ^(double firstValue, double secondValue) {
                                  return firstValue * secondValue;
                              };
 
    double result = multiplyTwoValues(2,4);
 
    NSLog(@"The result is %f", result);

Block可以截獲外部變量

除了包含可執(zhí)行代碼之外,Block還具有從其封閉的作用域內截獲變量狀態(tài)的能力。

例如,如果在方法中聲明一個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還是剛剛產(chǎn)生在堆上的Block(棧上Block的副本)都會引用該變量在堆上的副本。

你可以像下面這樣重寫當前的例子:

    __block int anInteger = 42;
 
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
    };
 
    anInteger = 84;
 
    testBlock();

因為變量anInteger被聲明為一個__block變量,它的內存地址與聲明中Block的變量地址是共享的。 這意味著日志輸出現(xiàn)在將顯示:

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作為方法或函數(shù)的參數(shù)

前面的每個例子都是在定義之后會立即調用Block。 在日常代碼編寫中,通常將Block作為參數(shù)傳遞給函數(shù)或方法以在其他地方進行調用。 例如,您可以使用GCD在后臺調用Block,或者定義一個要重復調用任務的Block,例如枚舉集合時。 并發(fā)和枚舉將在后面討論。

Block也用于回調,即定義任務完成時要執(zhí)行的代碼。 例如,您的應用程序可能需要通過創(chuàng)建執(zhí)行復雜任務的對象(例如從Web服務請求信息)來響應用戶操作。 因為任務可能需要很長時間,您應該在任務發(fā)生時顯示某種進度指示器(菊花),然后在任務完成后隱藏該指示器(菊花)。

當然,你可以使用委托來完成這個任務:你需要創(chuàng)建一個合適的委托協(xié)議,實現(xiàn)所需的方法,將你的對象設置為任務的委托,然后等待,一旦任務完成時它在你的對象上調用一個委托方法。

然而,Block可以讓這些更加容易,因為您可以在啟動任務時定義回調行為,如下所示:

- (IBAction)fetchRemoteInformation:(id)sender {
    [self showProgressIndicator];
 
    XYZWebTask *task = ...
 
    [task beginTaskWithCallbackBlock:^{
        [self hideProgressIndicator];
    }];
}

此示例調用一個方法來顯示進度指示器(菊花),然后創(chuàng)建任務并指示它開始。 回調Block指定任務完成后要執(zhí)行的代碼; 在這種情況下,它只是調用一個方法來隱藏進度指示器(菊花)。 注意,這個回調block截獲了self,以便能夠在調用時調用hideProgressIndicator方法。 在截獲self時要小心,因為它很容易創(chuàng)建一個strong類型的循環(huán)引用,詳情見后面的如何在block截獲了self后避免循環(huán)引用

在代碼可讀性方面,該Block使得在一個位置上很容易看到在任務完成之前和完成之后會發(fā)生哪些情況,從而避免需要通過委托方法來查找將要發(fā)生的事情。

此示例中顯示的beginTaskWithCallbackBlock:方法的聲明如下所示:

- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;

(void(^)(void))上一個沒有參數(shù)沒有返回值的Block。 該方法的實現(xiàn)可以以通常的方式調用Block:

- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
    ...
    callbackBlock();
}

Block作為方法的參數(shù),其所擁有的多個或一個參數(shù)在形式上應與單純的Block變量相同:

- (void)doSomethingWithBlock:(void (^)(double, double))block {
    ...
    block(21.0, 2.0);
}

Block應該始終作為方法的最后一個參數(shù)

如果方法中含有Block以及其他非Block的參數(shù), 那么Block參數(shù)應該始終作為方法的最后一個參數(shù)寫出,如:

- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;

這使得在指定Block內聯(lián)時更容易讀取方法的調用,如下所示:

    [self beginTaskWithName:@"MyTask" completion:^{
        NSLog(@"The task is complete");
    }];

使用類型定義來簡化Block語法

如果需要使用相同的Block類型來定義多個Block,您可能需要為該類型進行重新的定義。
例如,您可以為沒有參數(shù)沒有返回值的簡單Block定義類型(即為Block類型取一個別名):

typedef void (^XYZSimpleBlock)(void);

然后,可以使用自定義類型的Block作為方法的參數(shù)或用自定義類型來創(chuàng)建Block變量:

    XYZSimpleBlock anotherBlock = ^{
        ...
    };
- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
    ...
    callbackBlock();
}

自定義類型定義在處理作為返回值的Block或將其他Block用作參數(shù)的Block時特別有用。 請看以下示例:

void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
    ...
    return ^{
        ...
    };
};

complexBlock變量指的是將另一個Block作為參數(shù)(aBlock)并返回另一個Block的Block。
使用類型定義來重寫上面的代碼,這使的這段代碼更加可讀:

XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
    ...
    return ^{
        ...
    };
};

對象使用Block作為屬性

定義一個Block屬性的語法類似于聲明一個Block變量:

@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end

注意:您應該將copy指定為屬性修飾符,變量被Block截獲后,會改變自身在內存的位置,由棧區(qū)變?yōu)槎褏^(qū),所以Block也需要將自己復制到堆區(qū),以應對這種改變。 當使用自動引用計數(shù)時,你是不需要擔心的,因為它會自動發(fā)生的,但是屬性修飾符的最佳做法是顯示結果行為。 有關更多信息,請參閱Blocks Programming Topics

Block屬性的設置及調用和其他的Block變量是一樣的:

    self.blockProperty = ^{
        ...
    };
    self.blockProperty();

同時也可以使用類型定義的方式聲明一個Block屬性,如下:

typedef void (^XYZSimpleBlock)(void);
 
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end

<a id="no1"></a>如何在Block截獲了self后避免循環(huán)引用

如果在定義一個Block回調時,需要在Block中截獲self,內存管理的問題是需要引起重視的。

Block對任何截獲的對象都是強引用,包括self;記住這一點后,想要解開循環(huán)引用就不是很難了,如下,一個擁有Block屬性的對象,在Block內截獲了self

@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
    self.block = ^{
        [self doSomething];    // Block對self是強引用的
                               // 這就產(chǎn)生了循環(huán)引用
    };
}
...
@end

像上面這樣的一個簡單例子中,編譯器是會在你編寫代碼時報警告的;但是對于有多個強應用對象在一起產(chǎn)生的循環(huán)引用問題,編譯器是很難發(fā)現(xiàn)循環(huán)引用問題的:

為了避免出現(xiàn)這種問題,最好的方式是截獲一個弱引用的self,如下所示:

- (void)configureBlock {
    XYZBlockKeeper * __weak weakSelf = self; 
      
    //或__weak typeof(self) weakSelf = self;
    
    self.block = ^{
        [weakSelf doSomething];   // 截獲一個弱引用self
                                  // 以此來避免循環(huán)引用   
    }
}

通過在Block內截獲了一個弱指針指向的self,這樣Block就不會再維持對XYZBlockKeeper對象的強引用關系了。如果對象在Block被調用之前釋放了,指針weakSelf就會被置為空;

Block可以用來簡化枚舉

除了作為基本的回調使用外,許多的Cocoa 和 Cocoa Touch 框架的API也用Block來簡化任務,如集合枚舉。例如,NSArray就提供了三個含有Block的方法:

- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;

這個方法接受一個Block的參數(shù),這個參數(shù)對于數(shù)組中的每個項目調用一次:

    NSArray *array = ...
    [array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
        NSLog(@"Object at index %lu is %@", idx, obj);
    }];

上面的Block需要三個參數(shù),前兩個參數(shù)指向當前對象及其在數(shù)組中的索引。 第三個參數(shù)是一個指向布爾變量的指針,可以用來停止枚舉,如下所示:

    [array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
        if (...) {
            *stop = YES;
        }
    }];  

還可以使用enumerateObjectsWithOptions:usingBlock:方法自定義枚舉。 例如,指定NSEnumerationReverse這一選項將會反向遍歷集合。

如果枚舉Block中的代碼是處理器密集型(processor-intensive)并且是安全的并發(fā)執(zhí)行 -- 您可以使用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);
    }];

如上面的例子所示:相比使用傳統(tǒng)的循環(huán)遍歷,使用枚舉鍵值對的方式會更加方便,

Block可以用來簡化并發(fā)任務

每個Block代表一個不同的工作單元,就是可執(zhí)行代碼與Block周圍作用域中截獲的可選狀態(tài)組合。 這使的Block成為OS X和iOS中理想的異步并發(fā)調用可選項之一。 且無需弄清楚如何使用線程等低級機制,您可以使用Block定義任務,然后讓系統(tǒng)在處理器資源可用時執(zhí)行這些任務。

OS X和iOS提供了多種并發(fā)技術,包括兩種任務調度機制:Operation queues和GCD。 這些機制圍繞著一個等待被調用的任務隊列而設。 您按照需要調用它們的順序將Block添加到這一隊列中,當處理器時間和資源可用時,系統(tǒng)將對這一隊列中的Block進行調用。

串行隊列只允許一次執(zhí)行一個任務 -- 隊列中的下一個任務直到前一個任務完成才會被調用,在此期間這一任務將不會離開隊列。 并發(fā)隊列會調用盡可能多的任務,而不必等待前面的任務完成。

使用Block操作隊列

操作隊列是Cocoa和Cocoa Touch框架的任務調度方式。 您創(chuàng)建一個NSOperation實例來封裝一個工作單元以及任何必要的數(shù)據(jù),然后將該操作添加到NSOperationQueue中來執(zhí)行。

雖然您可以創(chuàng)建自己的自定義NSOperation子類來實現(xiàn)復雜的任務,但也可以通過NSBlockOperation使用Block的方式創(chuàng)建一個操作,如下所示:

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    ...
}];

您可以手動執(zhí)行操作,但操作通常添加到現(xiàn)有的操作隊列或您自己創(chuàng)建的隊列中去執(zhí)行:

// 在主隊列執(zhí)行任務:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
 
// 在后臺隊列執(zhí)行任務:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];

如果使用操作隊列,可以配置操作之間的優(yōu)先級或依賴關系,例如指定一個操作先不執(zhí)行,直到一組其他操作完成才執(zhí)行。例如,您還可以通過KVO的方式監(jiān)聽操作狀態(tài)的改變,然后在任務完成時,更新進度指示器(菊花):

更多關于操作和隊列操作的信息,見Operation Queues

使用GCD在調度隊列中給Block進行進度安排。

如果需要安排任意Block代碼執(zhí)行的話,您可以直接使用由Grand Central Dispatch(GCD)控制的調度隊列(dispatch queues)。 調度隊列使得相對于調用者同步或異步地執(zhí)行任務變得容易,并且以先進先出的順序執(zhí)行它們的任務。

您可以創(chuàng)建自己的調度隊列(dispatch queue)或使用GCD自動提供的隊列。 例如,如果需要安排并發(fā)執(zhí)行的任務,可以通過使用dispatch_get_global_queue()函數(shù)并指定隊列優(yōu)先級來獲取對現(xiàn)有隊列的引用,如下所示:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

//要將該block分派到隊列中,您可以使用dispatch_async()或dispatch_sync()函數(shù)。
// dispatch_async()函數(shù)不會等待要調用的block執(zhí)行完畢,而是立即返回:

dispatch_async(queue, ^{
    NSLog(@"Block for asynchronous execution");
});

更多關于隊列調度和GCD的問題見Dispatch Queues.

文章主要翻譯自Apple官方文檔Working with Blocks

喜歡的話,給個關注,謝謝!!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容