本次的主要內容是塊,對初學者來說,代碼中涉及到塊的內容確實很容易讓人疑惑。首先談一下塊的概念,塊(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 來修飾一個變量的時候,這個變量就會放到原本它所在的作用域以及所有塊所共享的存儲區中。
還是用之前的這個例子,我們把 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,其實我們也可以自己去定義一個隊列,這樣我們就可以非常靈活地拿到自己想要的隊列,並行或串行,同步或異步,優先級等等都可以靈活定制,當然,這些是以後的內容了。
對於 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;
這個方法去取一個塊作為參數,然後對集合內的每一個元素都執行這個塊。對於集合類來說,很多的方法都會采用塊作為參數。
關於塊的內容,整體來說的話,可以這麼去看,為了理解的方便,我們可以把它看做是一個函數指針,只是這個指針的特殊之處在於,它本身是一個對象,並且它和函數不同,它是一組可以執行的代碼單元。另外,塊可以捕獲外部的變量,但是這種捕獲是局部的捕獲,如果我們使用了全局變量,那麼它也會去捕獲全局變量,並且變量的捕獲是類似於快照一樣的,只是捕獲了一個值。如果想要在塊中實時的訪問一個變量,就要讓那個變量存儲在快的共享區中。最後,塊對於對象的引用,是強引用,所以為了避免強引用循環,我們需要主動把塊對對象的引用,改成弱引用。