Programming with Objective-C(六)

來源:互聯網
上載者:User

標籤:

本次的主要內容是塊,對初學者來說,代碼中涉及到塊的內容確實很容易讓人疑惑。首先談一下塊的概念,塊(Block)是蘋果為 C、C++以及 OC 添加的一種特性,它包含了部分代碼,可以被當做是參數傳遞給函數,並且它的實質是 OC 中的對象,也就是我們完全可以把它放到集合中,比如我們可以定義 NSArray 或者 NSDictionary 的對象來放置一系列的塊,然後通過代碼來決定執行哪一個塊。塊還有一大特性,就是可以從相應的代碼塊中截取變數的值,就像閉包或者 lambda 運算式一樣,但是塊所截取的只是單純的值。

文法

關於塊的聲明,直接使用一個 ^ 就可以了,就像這樣

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

這麼一看的話,其實塊和普通的代碼快並沒有多大的區別,但是塊卻具有代碼塊所無法實現的特性。打個比方,塊定義之後,它就會作為一個 OC 的對象而存在,而普通的代碼塊則做不到這樣。既然塊可以作為對象來存在,那麼我們就應該有辦法去擷取一個塊的對象,然後去使用它。對於這點,我們可以通過一個指標來實現:

void (^simpleBlock)(void);

其實這個指標的形式和函數指標的形式非常像,第一個 void 指明傳回型別,第二個 void 表明參數,然後中間是塊的名稱。考慮到塊的其他特性,其實我們可以把塊看做是一種特殊的函數。這裡我們還是繼續看一下這個指標,如果我們想讓它指向一個具體的代碼塊,只要這樣:

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

實際上,這就是一個賦值的過程,,當賦值完成後,我們就可以直接調用塊:

simpleBlock();
參數和傳回值

之前也提到了,塊可以看做是一個特殊的函數,那麼它自然也是可以定義參數以及傳回型別的,並且前面也提到了塊的定義中各個部分代表的意義,按照前面的說明,如果我們要給塊定義參數和傳回值,那麼應該是這種形式:

double (^multiplyTwoValues)(double, double);

這麼看的話,確實塊指標的聲明和函數指標的聲明幾乎是一模一樣。並且在返回的時候也是通過 return 關鍵字來返回相應資料。另外,對於塊的調用也和函數的調用非常相似:

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

所以在對塊的概念還是感到疑惑,難以理解的時候,不妨先把它看做是一種特殊的函數。

值的截取

前面也有說到,塊中可以截取相應範圍內的值。比如在一個函數中定義了一個塊,那麼它會儲存當前時刻這個函數範圍內的狀態。其中包含了範圍內的值,所以可以直接使用:

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

但是關於這種截取是基於值的,也就是說我們在塊中使用的值,可能並不是這個變數當前的值,而是在塊建立的那個時候所截取的一個記錄。這也就是說,在塊中我們沒有辦法即時地擷取外部變數的值,並且也沒有辦法去修改變數的值,就像下面這樣:

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

這一段代碼最終輸出的結果是 Integer is: 42,所以很明顯我們並沒有直接擷取這個變數的值,只是儲存了一份副本。另外,在塊中我們也無法修改截取到的變數的值,因為它是 const 類型的。

使用 __block 來分享變數

之前說到,關於塊中截取到的變數,我們既無法擷取到它即時的值,也無法對擷取到的值做出修改,但是對於原本的變數,我們可以使用 __block 來進行修飾。這個聲明實際上是針對儲存的一種聲明,當我們用 __block 來修飾一個變數的時候,這個變數就會放到原本它所在的範圍以及所有塊所共用的儲存區中。

還是用之前的這個例子,我們把 anInteger 改成一個在塊間共用的變數:

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

這一次啟動並執行結果就變成了 anInteger,也就是我們已經即時擷取倒了變數的值,並且,這個時候我們也可以在塊的內部修改變數的值了。

把塊作為參數

實際上,我們之所以定義了塊,並不是為了在定義之後就去直接調用,更多的時候,我們只是希望在某個操作之後,可以根據這個操作的結果來調用一段代碼,但是這麼一段代碼我們沒有辦法預先定義好,所以這個時候就可以用到塊,這也是塊比較重要的一個特性,那就是作為參數來傳遞。一般來說,我們在傳遞塊的時候,主要是把它作為回調的內容或者是用作多線程的開發。

我們可以用一個很簡單的小例子來說明一下,比如說網路請求,一般來說在進行網路請求的時候,我們都會先放出一個載入框,等到請求完成之後再關閉載入框,那麼我們就可以這樣去實現這個過程:

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

這裡涉及到了一個 self 的問題,在塊中,我們看到,對於 self 變數沒有做任何的處理,就直接調用了相應的方法,實際上,對於塊中出現的 self,是要多加小心的,因為非常容易產生強引用迴圈。

這裡先看一下 beginTaskWithCallbackBlock 這個函數,其實這個函數比較簡單,需要在意的是他的聲明,我們先看一下這個函數的申明:

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

實際上和塊的聲明基本類似,第一個 void 指明傳回型別,第二個 void 說明參數類型,中間表明是塊。和塊的聲明不同的地方在於,塊的名稱放到了最後面。

塊作為參數時應當放到參數列表的最後面

這個可以算是一種編碼規範了,在編寫函數的時候,如果參數中有塊,那麼就放到最後面,畢竟這樣在調用的時候,代碼看起來更加簡潔明了。一般對於塊的命名,要麼像之前一樣使用 callback,要麼直接叫做 completion,基本上塊的命名就這麼兩種。

通過自訂類型來簡化塊的文法

塊變數的聲明其實挺麻煩的,尤其是如果要聲明多個塊的變數的時候,一遍又一遍重複地寫傳回型別、參數,估計也是挺要命的。所以如果要簡化這裡的寫法,我們可以預先定義好一個對應塊的類型,也就是使用 typedef 關鍵字來實現:

typedef void (^XYZSimpleBlock)(void);

像上面這樣,就定義了一個傳回值、參數都為空白的一種塊類型 XYZSimpleBlock,接下來去定義塊的類型的時候,就可以直接定義:

XYZSimpleBlock anotherBlock = ^{    ...};

當然,就這麼看的話感覺好像自訂類型並沒有什麼用,但是如果代碼中會涉及到多個同種類的塊的時候,這樣做就方便很多了。並且在函數中涉及到塊的參數,這裡的定義就方便很多了:

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

上面的代碼基本上就和普通的函數參數聲明沒什麼區別了,一下子簡化了很多。

當然,還有另一種情況下自訂一個類型是非常有必要的,比如,如果說一個塊的傳回值,是另一個塊,可以先考慮一下這個要怎麼寫。標準的塊的聲明是這樣的 (傳回值)(^塊名)(參數),那麼如果把傳回值換成另一個塊再看看:

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

說實話,看到這段代碼我個人是覺得挺頭痛的,這段代碼基本上就是一個塊,它會返回一個 (void (^) (void)) 類型的塊,再看看類型的定義,整個人頭都大了,這個塊本身還用了另外一個塊作為參數,基本上就是不能讓人好好玩耍的狀態,但是,假如我們自訂了 void (^) (void) 這個類型,再看看這段代碼會變成什麼樣:

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

一下子整個代碼都清晰了,有時候可讀性就是這麼重要。

將塊設定為屬性

實際上之前也提到了,塊有一個很有趣的地方,那就是它本身其實也是一個 OC 中的對象,所以我們完全可以把它當做是一個類的屬性。如果說要把一個塊作為一個類的對象,那麼得考慮清楚它的作用,否則單單為了某一個函數的回調特意去設定一個屬性意義不大。當然,這裡主要只是說明用法:

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

由於塊的特殊性,所以在聲明的時候我們沒辦法直接寫一個簡單的類型,除非是自訂,否則只能像這樣整個寫出來。另外值得注意的一點就是,如果要把塊當做一個屬性,那就要把它設定為 copy 的,這是因為當一個塊在捕獲外部域的狀態的時候,一個塊會被複製,這個過程有點類似於快照,我們儲存的其實是當時的一種狀態。把塊作為對象的屬性之後,它的使用其實也沒有太大的變動:

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

當然,如果配合一下自訂類型的話,看起來會更好:

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

對於塊的理解,有一點不能忘記,那就是它的本質是 OC 的一個對象。

避免強引用迴圈

看到這裡很多人都會覺得奇怪,塊在捕獲變數的時候類似於快照,為什麼還會產生強引用迴圈?實際上,在塊對變數進行捕獲的時候,它對對象產生的是一個強引用,也許這聽上去很奇怪,不過,畢竟我們有辦法讓變數處於塊的儲存區之中,而塊可以去直接存取這樣的變數,所以為了保險,在捕獲的時候就直接採用強引用避免在塊中代碼執行到一半的時候對象已經被銷毀了。再回過頭來審視一下強引用迴圈,因為塊也是一個對象,所以如果產生了強引用迴圈,那麼就是對象間相互引用的狀況,再結合之前的可以把塊當做屬性,強引用迴圈產生的原因也就很明確了。關鍵問題就在於如何解決,其實和普通的強引用迴圈一樣,解決方案就是加入弱引用迴圈,但是問題在於把哪一方設定成為弱引用。考慮在一個塊的內部,這個時候塊本身肯定是不會釋放的,並且此時它持有了一個對象的強引用,這個對象又保持了對塊的一個強引用,這意味著單單考慮塊中的情況而言,塊要釋放,就必須先把對象對它的強引用撤銷掉,因為塊不執行完是不可能被消除的,所以我們需要做的,就是塊對對象的引用改成弱引用,這一點可以通過一個小方法做到:

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

透過這段代碼,我們還可以發現另外一件事,那就是塊對變數的捕獲方式,因為這裡其實我們捕獲到的是 weakSelf,而不是 self,否則強引用迴圈依舊存在,這也就是說塊對變數的捕獲並不是站在全域的,而是局部的捕獲。

塊的常見用法

談到塊的用法,首先會想到的就是回調,沒錯,塊經常用於函數的回調,因為它正好可以當做一個執行工作的單元,這樣一來通過塊來實現一些非同步操作就非常方便。

說到這裡的話,先簡單的說一下 OC 中的多線程。OC 中的多線程嚴格意義上可以說就兩種,C 語言中的 Thread, OC 提供的操作隊列。如果放寬一點的話,iOS 和 OS X 中可以對應 posix 的線程標準,通過 iOS 中還有個 GCD,其實它實質上也是操作隊列,只是官方為我們封裝好的固定的隊列。多線程具體的內容後續還會詳細說明,現在就點到為止。

操作隊列

回過頭來再接著看塊,在使用操作隊列的時候,我們通常都是建立一個 NSOperation 的執行個體,這個執行個體其實就是封裝了某些操作,接著我們就會把它加入 NSOperationQueue 這個隊列中執行。關於 NSOperation 的使用,大體上如下:

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

可以看到,我們在執行個體化 NSOperation 的時候就是直接使用塊來進行執行個體化,再考慮到 NSOperation 的定義,其實塊非常符合操作隊列的要求。接下來可以看看操作隊列的用法:

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

這裡直接取了一個 mainQueue,其實我們也可以自己去定義一個隊列,這樣我們就可以非常靈活地拿到自己想要的隊列,並行或串列,同步或非同步,優先順序等等都可以靈活定製,當然,這些是以後的內容了。

GCD

對於 iOS 開發來說,GCD 應該是非常熟悉的了,iOS 中幾大多線程編程,GCD 應該是最方便的,系統為我們製作的每個應用程式都準備好了幾個隊列,想用的時候直接拿就可以了,並且常用的隊列都覆蓋到了,當然,基於 GCD 我們也可以自己來定義一個隊列,言歸正傳,還是來看看 GCD 的用法:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

就這麼一句簡單的話,我們就擷取到了一個隊列,並且我們可以通過參數來告訴系統我們要什麼樣的隊列。然後,我們就可以開始執行操作了:

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

實際上對於 Cocoa 以及 Cocoa Touch 中的 API,它們都會接受一個塊作為對象來簡化處理過程,比如對枚舉這樣的集合來說。以 NSArray 作為一個例子:

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

這個方法去取一個塊作為參數,然後對集合內的每一個元素都執行這個塊。對於集合類來說,很多的方法都會採用塊作為參數。

總結

關於塊的內容,整體來說的話,可以這麼去看,為了理解的方便,我們可以把它看做是一個函數指標,只是這個指標的特殊之處在於,它本身是一個對象,並且它和函數不同,它是一組可以執行的代碼單元。另外,塊可以捕獲外部的變數,但是這種捕獲是局部的捕獲,如果我們使用了全域變數,那麼它也會去捕獲全域變數,並且變數的捕獲是類似於快照一樣的,只是捕獲了一個值。如果想要在塊中即時的訪問一個變數,就要讓那個變數儲存在快的共用區中。最後,塊對於對象的引用,是強引用,所以為了避免強引用迴圈,我們需要主動把塊對對象的引用,改成弱引用。

Programming with Objective-C(六)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.