使用塊 - Working with Blocks

2018-08-12 21:19 更新

使用塊 - Working with Blocks

一個(gè) object-c 類定義了一個(gè)對(duì)象,這個(gè)對(duì)象整合了與行為相關(guān)的數(shù)據(jù)。有時(shí),它僅僅代 表了一個(gè)簡(jiǎn)單的小任務(wù)或是一個(gè)行為單元而不是一個(gè)方法的集合。 區(qū)塊是添加在 c,object-c 和 c++ 語言中的語言級(jí)別的形式,它允許你編寫一個(gè)獨(dú)特的代 碼段,這個(gè)代碼段能夠在作為值方法和函數(shù)中傳遞。塊是 object-c 的對(duì)象,這意味著他 們能夠被添加到像 NASArray 或是 NSDictionary 的集合中。他們也有從作用域中捕獲值的 能力,使他們與其他編程語言關(guān)于 toclosures 或 lambda 表達(dá)式上的作用非常類似。 本章闡釋了定義和引用塊的語法,也展示了如何使用塊簡(jiǎn)化一般的任務(wù) ,比如集合枚舉。欲了解更多的信息,請(qǐng)參閱Blocks Programming Topics。

塊語法

塊的定義語法是用脫字符(^)來定義的,如下:

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

函數(shù)和方法定義時(shí),大括號(hào)表明了塊的起點(diǎn)和終點(diǎn)。在本例中,該塊不返回任何值,也沒有任何參數(shù)。 你也可以用同樣的方式函數(shù)指針指向一個(gè)c函數(shù),聲明一個(gè)變量來跟蹤塊,如下:

    void (^simpleBlock)(void);

如果你沒有接觸過c函數(shù)指針,那么這里的語法你也許會(huì)覺得有一些困擾。這個(gè)例子聲明了一個(gè)變量 called simpleBlock 來調(diào)用一個(gè)沒有參數(shù)和返回值的塊,這意味著變量能夠被像上面那樣的塊進(jìn)行賦值,如下:

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

這就像其他任何的變量賦值,所以語句必須被以右大括號(hào)后面的分號(hào)來進(jìn)行結(jié)束。你也可以把變量的聲明和賦值進(jìn)行合并:

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

一旦你已經(jīng)聲明和賦值了一個(gè)塊變量,你能夠用它來調(diào)用這個(gè)塊:

simpleBlock();

提示:如果你試圖用沒有被賦值的變量來調(diào)用一個(gè)塊(零塊變量),你的應(yīng)用將會(huì)崩潰。

有參數(shù)和返回值的塊

塊也可以有參數(shù)和返回值就像其他的方法或是函數(shù)。 舉一個(gè)例子,考慮一個(gè)變量來引用返回兩個(gè)變量相乘結(jié)果的塊:

double (^multiplyTwoValues)(double, double);

相應(yīng)的塊文字可能是這樣的:

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

就像其他函數(shù)的定義那樣,當(dāng)塊被調(diào)用時(shí) firstValuesecondValue 是用來計(jì)算結(jié)果值的。在這個(gè)例子中,返回值類型是由塊中返回語句來決定的。

根據(jù)你的個(gè)人所好,你可以通過在脫字符之后明確寫出返回值類型:

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

一旦你已經(jīng)聲明或定義了一個(gè)塊,你就能像一個(gè)函數(shù)一樣來調(diào)用他:

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

    double result = multiplyTwoValues(2,4);

    NSLog(@"The result is %f", result);

塊能夠從封閉區(qū)域中捕捉值

和包含可執(zhí)行代碼一樣,塊也有能夠從封閉區(qū)域捕捉的能力。

比如,如果你在一個(gè)方法中聲明了塊,他可以捕捉到任何在方法域中可以訪問到的值,如下:

\- (void)testMethod {
    int anInteger = 42;

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

    testBlock();
}

在這個(gè)例子中 anInteger 在塊外進(jìn)行定義,但是當(dāng)塊被定義時(shí),該值也是被捕獲了。

除非另行指定,值是一定會(huì)被捕獲的。這意味著如果你在定義塊的時(shí)間點(diǎn)和調(diào)用塊的時(shí)間點(diǎn)之間改變外部變量的值,如下:

  int anInteger = 42;

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

    anInteger = 84;

    testBlock();

被塊捕獲的值是不受影響的。這意味著輸出結(jié)果仍然會(huì)顯示:

Integer is: 42

它同樣意味著塊不能夠改變初始變量的值,或者是被捕獲的值(被當(dāng)做常量)。

使用_ _block變量共享存儲(chǔ)

如果您需要能夠從一個(gè)塊中改變捕獲變量的值,你可以使用 _ _block 存儲(chǔ)類型修飾符對(duì)原變量聲明。這意味著變量是存在于在作用域范圍之內(nèi)聲明的塊共享的存儲(chǔ)空間。

舉個(gè)例子,你可能會(huì)改寫先前的例子,如下:

__block int anInteger = 42;

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

anInteger = 84;

testBlock();

由于 anInteger 聲明為 __block 變量,其存儲(chǔ)與塊聲明共享。這意味著,在日志的輸出將現(xiàn)在顯示:

Integer is:84

這也意味著,該塊可以修改原始值,如下:

    __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

你可以把塊作為方法或函數(shù)的參數(shù)

本章前面的例子,都是再定義一個(gè)塊后,立即就調(diào)用它。實(shí)際中更常見的是在其他地方收到指令后在把塊傳遞給方法或函數(shù)。比如,你可能用 GCD 技術(shù)來在后臺(tái)調(diào)用一個(gè)塊,或者定義一個(gè)塊來表示一個(gè)任務(wù)被反復(fù)調(diào)用。比如,列舉的集合時(shí)。并發(fā)性和枚舉將本章后面介紹。

塊也用于回調(diào),定義了當(dāng)一個(gè)任務(wù)結(jié)束時(shí)將被執(zhí)行的代碼。舉一個(gè)例子,你的應(yīng)用程序可能需要通過創(chuàng)建一個(gè)對(duì)象,執(zhí)行一個(gè)復(fù)雜的任務(wù),諸如從web服務(wù)請(qǐng)求信息,以響應(yīng)用戶操作。因?yàn)槿蝿?wù)可能需要很長(zhǎng)的時(shí)間,任務(wù)正在發(fā)生時(shí),你應(yīng)該顯示某種進(jìn)度指示器。一旦任務(wù)完成,就要來隱藏該指示器。

這將有可能使用授權(quán)做到這一點(diǎn):你需要?jiǎng)?chuàng)建一個(gè)合適的委托協(xié)議,實(shí)施必要的方法,設(shè)置你的對(duì)象作為任務(wù)的代表,一旦任務(wù)完成了,那么就在你的對(duì)象中等待它調(diào)用委托方法。

使用塊將使這方面內(nèi)容非常容易,因?yàn)槟憧梢栽谀愠跏蓟愕娜蝿?wù)時(shí),來定義回調(diào)行為,如下:

- (IBAction)fetchRemoteInformation:(id)sender {
    [self showProgressIndicator];

    XYZWebTask *task = ...

    [task beginTaskWithCallbackBlock:^{
        [self hideProgressIndicator];
    }];
}

這個(gè)例子調(diào)用一個(gè)方法來顯示進(jìn)度指示器,然后創(chuàng)建任務(wù),并告訴它啟動(dòng)。毀掉快明確了在任務(wù)完成后將要執(zhí)行的代碼;在這種情況下,調(diào)用一個(gè)方法來隱藏進(jìn)度指示器是十分簡(jiǎn)單的。請(qǐng)注意,此回調(diào)塊捕獲自己以便能夠在被調(diào)用時(shí)能夠調(diào)用 hideProgressIndicator 方法。捕獲自己時(shí)應(yīng)當(dāng)非常小心,因?yàn)檫@非常容易形成一個(gè)強(qiáng)引用循環(huán),這將在后面的“在捕捉自時(shí)避免強(qiáng)引用循環(huán)”中詳細(xì)描述。

在代碼的可讀性方面,該塊可以在任務(wù)完成前和任務(wù)完成后很容易地看到在一個(gè)地方會(huì)發(fā)生什么。避免了通過跟蹤和委托方式來查明將要放生什么的需要。

聲明 beginTaskWithCallbackBlock:在這個(gè)例子中所示的方法是這樣的:

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

在(void (^)(void)) 指定的參數(shù)是塊不帶任何參數(shù)或者返回任何值。該方法的實(shí)現(xiàn)可以用普通的方法來調(diào)用塊:

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

用和塊變量一樣的方法來確定以一個(gè)或多個(gè)參數(shù)的塊為參數(shù)的方法:

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

塊應(yīng)該總是一個(gè)方法的最后一個(gè)參數(shù)

一個(gè)方法僅使用一個(gè)塊參數(shù)為好。如果方法還需要其他的非塊參數(shù),塊應(yīng)該放在參數(shù)的最后一個(gè)。

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

這使得指定塊內(nèi)嵌時(shí)的方法調(diào)用更容易閱讀,如下:

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

使用類型定義簡(jiǎn)化塊語法

如果您需要定義具有相同簽名多個(gè)塊,你可能想給這個(gè)簽名定義自己的類型。 舉一個(gè)例子,你能夠定義一個(gè)類型來創(chuàng)建一個(gè)沒有參數(shù)和返回值的塊,如下:

typedef void (^XYZSimpleBlock)(void);

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

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

    ...
    callbackBlock();
}

當(dāng)處理以塊為參數(shù)或返回值類型的塊是定義自定義類型尤其重要,考慮如下的例子:

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

complexBlock 變量是指一個(gè)塊需要另一塊作為一個(gè)參數(shù)(ABLOCK)并返回另一個(gè)塊。

重寫代碼使用類型定義使其可讀性更高:

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

對(duì)象用屬性來跟蹤塊

定義一個(gè)屬性來跟蹤塊的語法和定義塊變量的語法是相似的:

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

注意:你應(yīng)該指定副本的屬性特征,因?yàn)橐粋€(gè)塊需要被復(fù)制來跟蹤其在原始范圍外抓獲的狀態(tài)。 使用自動(dòng)引用計(jì)數(shù)的時(shí)候,你就不必這樣做了,因?yàn)樗鼤?huì)自動(dòng)發(fā)生,但屬性特征最好是來顯示 其必然行為。想要得到更多的相關(guān)信息,請(qǐng)參考?jí)K編程條目。

一個(gè)塊屬性被初始化或調(diào)用的方法和其他塊變量是相似的:

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

它也可以使用塊屬性聲明的類型定義,如下:

typedef void (^XYZSimpleBlock)(void);

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

在捕捉自時(shí)避免強(qiáng)引用循環(huán)

如果你需要在塊中捕捉自,比如說定義一個(gè)回調(diào)塊,考慮內(nèi)存管理的影響十分重要。

塊對(duì)任何捕獲的對(duì)象都保持了強(qiáng)引用,包括自己,這意味著它很容易結(jié)束了一個(gè)很強(qiáng)的參考周期 舉個(gè)例子,一個(gè)對(duì)象維護(hù)一個(gè)捕獲自的塊的副本屬性:

@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

編譯器會(huì)警告你的簡(jiǎn)單錯(cuò)誤可能是這樣的,但更復(fù)雜的例子可能涉及的對(duì)象創(chuàng)建周期之間的多個(gè)強(qiáng)引用,使之更難以診斷。為了避免這個(gè)問題,捕獲一個(gè)弱參考是比較好的方法,如下:

- (void)configureBlock {
    XYZBlockKeeper * __weak weakSelf = self;
    self.block = ^{
        [weakSelf doSomething];   // capture the weak reference
                                  // to avoid the reference cycle
    }
}

通過捕獲自我弱指針,塊將無法保持牢固的關(guān)系去回到XYZBlockKeeper對(duì)象。如果該對(duì)象在 快被調(diào)用前釋放,weakSelf指針會(huì)簡(jiǎn)單地設(shè)置為零。

塊可以簡(jiǎn)化枚舉

除了一般完成處理程序,許多 cocoa 和 cocoa Touch API 用塊來簡(jiǎn)化一般的任務(wù),如集合枚舉 像是 NSArray 類,提供了三個(gè)基于塊的方法,包括:

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

這個(gè)方法有一個(gè)塊參數(shù),對(duì)每個(gè)項(xiàng)目來說只掉用一次。

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

塊本身有三個(gè)參數(shù),前兩個(gè)參數(shù)表明了當(dāng)前的對(duì)象和他在陣列中的索引。第三個(gè)參數(shù)為一個(gè)指向波爾變量的指針,

你可以用它來停止枚舉:

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

它也可以通過使用 enumerateObjectsWithOptions 定制枚舉。指定 theNSEnumerationReverse選項(xiàng)。例如,以相反的順序遍歷集合。

如果枚舉代碼塊是密集處理的,并且能安全并發(fā)執(zhí)行,你可以使用 NSEnumerationConcurrent選項(xiàng):

    [array enumerateObjectsWithOptions:NSEnumerationConcurrent
                            usingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
        ...
    }];

這個(gè)標(biāo)志指示枚舉塊調(diào)用可以分布在多個(gè)線程,如果代碼塊是密集處理型,那么該調(diào)用將提供一個(gè)潛在的性能提升。請(qǐng)注意,使用此選項(xiàng)時(shí),枚舉順序是不確定的。

 NSDictionary 類同樣提供基于塊的方法,包括:
 NSDictionary *dictionary = ...
    [dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) {
        NSLog(@"key: %@, value: %@", key, obj);
    }];

舉個(gè)例子來說,相對(duì)于傳統(tǒng)的循環(huán),這樣做能夠更方便的枚舉所有鍵值對(duì)。

模塊可以簡(jiǎn)化并發(fā)任務(wù)

塊代表了一個(gè)不同的工作單元,綜合了可執(zhí)行代碼和周圍范圍可捕獲的狀態(tài)。這使得它非常適合使用OS X和iOS提供的并發(fā)選項(xiàng)中的異步調(diào)用。你可以使用塊簡(jiǎn)單地定義你的任務(wù),然后讓系統(tǒng)執(zhí)行這些任務(wù)的處理器資源可用,而不必弄清楚像線程低層次的機(jī)制怎樣使用。

OS X和ios提供了多種并發(fā)技術(shù),其中包括兩個(gè)任務(wù)調(diào)度機(jī)制:操作隊(duì)列和中央調(diào)度。這些機(jī)制是圍繞著等待被調(diào)用的任務(wù)隊(duì)列的一個(gè)想法。你按照調(diào)用順序?qū)⒛愕膲K加入隊(duì)列,然后在處理器資源可用時(shí),你的系統(tǒng)將從隊(duì)列中取出調(diào)用。

一個(gè)串行隊(duì)列只允許同時(shí)執(zhí)行一個(gè)任務(wù)--下一個(gè)任務(wù)將在上一個(gè)任務(wù)完成后才會(huì)在隊(duì)列中取出調(diào)用。一個(gè)并發(fā)隊(duì)列調(diào)用可以包括非常多的任務(wù),并且不用等待前一個(gè)人物執(zhí)行完畢。

使用塊操作與運(yùn)行隊(duì)列

一個(gè)運(yùn)行隊(duì)列是 Cocoa 和 Cocoa Touch 提供的任務(wù)調(diào)度。要?jiǎng)?chuàng)建的 NSOperation 實(shí)例來封裝工作單元以填充必要數(shù)據(jù)然后將該操作加入 NSOperationQueen 來執(zhí)行。

盡管你可以創(chuàng)建有自己的自定義NSOperation子類來實(shí)現(xiàn)復(fù)雜任務(wù),但也可以通過使用 NSBlockOperation 和塊來創(chuàng)建操作,如下:

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

這雖然是可以手動(dòng)執(zhí)行的操作,但是操作也會(huì)被添加在正準(zhǔn)備執(zhí)行的已經(jīng)存在的運(yùn)行隊(duì)列或是你自己創(chuàng)建的運(yùn)行隊(duì)列:

// 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];

如果您使用的操作隊(duì)列,可以配置操作的優(yōu)先級(jí)或依賴性,比如限制一個(gè)操作在其他幾項(xiàng)操作運(yùn)行完成后才能執(zhí)行。你同樣可以通過鍵值觀察來監(jiān)視操作的狀態(tài)改變,這樣,當(dāng)一個(gè)任務(wù)完成時(shí),可以更加容易的更新進(jìn)度指示器。 想要得到更多的關(guān)于操作運(yùn)行隊(duì)列的信息,請(qǐng)參考Operation Queues。

用GCD技術(shù)在調(diào)度隊(duì)列調(diào)度塊

如果你需要調(diào)用任意代碼塊來執(zhí)行,你可以直接利用GCD技術(shù)控制的調(diào)度隊(duì)列。對(duì)于請(qǐng)求者,調(diào)度隊(duì)列可以很容易地同步或異步執(zhí)行任務(wù),并以先入先出的順序執(zhí)行他們的任務(wù)。

你可以創(chuàng)建你自己的調(diào)度隊(duì)列,或是利用GCD提供的自動(dòng)生成的隊(duì)列。比如,如果你需要調(diào)度一個(gè)并發(fā)執(zhí)行的任務(wù),你能從已經(jīng)存在一個(gè)隊(duì)列中得到參考,通過使用 dispatch_get_global_queue()函數(shù)來調(diào)節(jié)隊(duì)列優(yōu)先級(jí),如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

你可以用 dispatch_async() 方法或是 dispatch_sync() 方法,調(diào)度塊到隊(duì)列中。 dispatch_async() 方法不用等到塊被調(diào)用會(huì)直接立即返回。

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

dispatch_sync() 方法不會(huì)返回指導(dǎo)塊完成執(zhí)行;比如,在一個(gè)需要等待其他工作完成才能繼續(xù)的并發(fā)塊中你可以運(yùn)用該方法。

想要得到更多關(guān)于 GCD 和調(diào)度隊(duì)列的信息,請(qǐng)參考 Dispatch Queues。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)