轉自:https://www.jianshu.com/p/7d32ed28292f
前言
老實說在早前我已經學會了如何使用 Block 來做一些方法回調,傳遞參數的功能,并且用 Block 簡單封裝了第三方的網絡庫(AFNetworking)。雖說對 Block 的應用說不上得心應手,但是卻是極其地喜歡使用這種設計模式,并且在項目中也大量地使用了。
但是,最近一位即将參加面試的學弟問我,什麼是 Block 呢?我蒙圈了,但是畢竟是學長,我假裝淡定地反問道:你所了解的 Block 是什麼呢?學弟說:是一段封裝的代碼塊,并可以放在任意位置使用,還可以傳遞資料。我心裡暗喜,這孩子還是圖樣了,于是語重心長地說:Block 的本質是可以截取自動變量的匿名函數。但是說出這句話我就後悔了,這句話他喵的到底是個什麼意思?看着學弟滿意地走了之後,我就瘋狂地上網查資料,萬一下次這個熊孩子深究起來可不就破壞了我英明神武的形象了,但是并沒有很滿意的答案,大多是照文檔描述了 Block 的定義以及基本用法,不然就是高深地去探讨 Block 底層的實作機制,顯然這些都不适合讓一個初學者既能學會使用又能夠沒有疑惑地使用。
本文主要講的是 Block 回調的使用,以及 Block 是如何實作這種神奇的回調兩部分來講的。
Block 回調實作
不着急,先跟着我實作最簡單的 Block 回調傳參的使用,如果你能舉一反三,基本上可以滿足了 OC 中的開發需求。已經實作的同學可以跳到下一節。
首先解釋一下我們例子要實作什麼功能(其實是爛大街又最形象的例子):
有兩個視圖控制器 A 和 B,現在點選 A 上的按鈕跳轉到視圖 B ,并在 B 中的textfield 輸入字元串,點選 B 中的跳轉按鈕跳轉回 A ,并将之前輸入的字元串
顯示在 A 中的 label 上。也就是說 A 視圖中需要回調 B 視圖中的資料。
想不明白的同學可以看一看最終實作的效果圖:
block example
這裡不再對 Block 的文法做說明了,不了解的同學可以點傳送門。
首先,我們需要定義兩個試圖控制器 AViewController 和 BViewController,現在我們需要思考一下,Block 應該在哪裡定義呢?
我們可以簡單地這樣思考,需要回調資料的是 A 視圖,那麼 Block 就應該在 B 中定義,用于擷取傳入回調資料。
是以我們在 BViewController.h 中定義如下:
//BViewController.h
#import <UIKit/UIKit.h>
typedef void(^CallBackBlcok) (NSString *text);//1
@interface BViewController : UIViewController
@property (nonatomic,copy)CallBackBlcok callBackBlock;//2
@end
在這裡,代碼 1 用 typedef 定義了
void(^) (NSString *text)
的别名為
CallBackBlcok
。這樣我們就可以在代碼 2 中,使用這個别名定義一個 Block 類型的變量
callBackBlock
。
在定義了
callBackBlock
之後,我們可以在 B 中的點選事件中添加
callBackBlock
的傳參操作:
//BViewController.m
- (IBAction)click:(id)sender {
self.callBackBlock(_textField.text); //1
[self.navigationController popToRootViewControllerAnimated:YES];
}
這樣我們就可以在想要擷取資料回調的地方,也就 A 的視圖中調用 block:
// AViewController.m
- (IBAction)push:(id)sender {
BViewController *bVC = [self.storyboard instantiateViewControllerWithIdentifier:@"BViewController"];
bVC.callBackBlock = ^(NSString *text){ // 1
NSLog(@"text is %@",text);
self.label.text = text;
};
[self.navigationController pushViewController:bVC animated:YES];
}
代碼 1 中,通過對回調将 B 中的資料傳遞到代碼塊中,并指派給 A
中的 label,實作了整個回調過程。
上例是通過将 block 直接指派給 block 屬性,也可以通過方法參數的方式傳遞 block 塊。
完整的示例代碼放在 git.oschina.net 上,代碼位址:BlockMagic 。
關于 Block 的疑惑
到目前為止,一切看起來都很美好(如果你照着上面的例子做的話),功能正常, A 視圖中也擷取到資料了。但是某些人可能就要說了,你的代碼有問題,你的思路有問題,你這是誤人子弟。
是的,代碼的确還有問題,第一個問題就是循環引用的問題,在 A 視圖的block 代碼塊中:
bVC.callBackBlock = ^(NSString *text){
NSLog(@"text is %@",text);
self.label.text = text;
};
代碼
self.label.text = text;
,在 Block 中引用 self ,也就是 A ,而 A 建立并引用了 B ,而 B 引用
callBackBlock
,此時就形成了一個循環引用,而編譯器也不會報任何錯誤,我們需要非常小心這個問題(面試百分百問到我會亂說?)。此時我們通常的解決方法是使用弱引用來解除這個循環:
__weak AViewController *weakSelf = self;
bVC.callBackBlock = ^(NSString *text){
NSLog(@"text is %@",text);
// self.label.text = text;
weakSelf.label.text = text;
};
第二個問題是我自己對 Block 的了解不到位,我們都知道 Block 能截取自動變量,并且是不能在 Block 塊中進行修改的(除非用
__block
修飾符),但是很明顯
weakSelf.label.text
的值被修改了,并且沒有用
__block
修飾符, 這是為什麼呢?因為
label
是個全局變量,而如果像如下的局部變量
a
是不能修改的,編譯器也會報錯:
局部變量
通過這個小例子發現的兩個問題,也算是值得了。
Block 為什麼能實作神奇的回調
在這裡我不會說什麼實作原理,僅僅是個人對 Block 能實作神奇回調的了解,有錯誤的地方請大家指出。
在先前使用 Block 的過程中,雖然會使用,但是總是有一個疑惑,簡單說來就是:
為什麼在 A 中的 block 塊能調用到 B 中的資料?
回顧一下我們在 B 中所實作的代碼,不外乎定義了一個 Block 變量,并在适當的時候傳入參數,那麼為什麼在調用了
self.callBackBlock(_textField.text)
之後,值就神奇傳到了 A 中的 Block 塊了呢?
通過整理使用的過程,我發現是我們的思維陷入了誤區(可能是我個人),我們認為在 B 中傳入
_textField.text
參數之後, A 中的 Block 塊就可以擷取到值。雖然思路是對的,但其實是不完整,導緻我們形成了回調的資料是通過某種底層實作傳遞過去的錯覺,這就使得我們認為這不需要深究。
事實是,通過簡單的整理我們可以發現完整的回調流程應該是這樣的:
回調流程
- block 代碼塊指派給
,此時bVC.callBackBlock
的指針就指向這個代碼塊。callBackBlock
- 調用
callBackBlock(NSString *text)
- 由于
的指針是指向 A 中的 block 代碼塊,是以執行代碼塊的代碼,實作回調。callBackBlock
很顯然之前我忽略了代碼塊指派給
callBackBlock
的這個操作(羞愧)。
現在再通過一段代碼可以更清晰地了解這個原理:
bVC.callBackBlock = ^(NSString *text){ //1
NSLog(@"text is %@",text);
};
bVC.callBackBlock = ^(NSString *text){ //2
NSLog(@"text b is %@",text);
};
上述代碼中,我們對
callBackBlock
進行了兩次指派,結果會怎麼樣呢?
two block
可以看出來,Block 的回調隻對代碼 2 生效,因為
callBackBlock
的指針最後指向了代碼 2 的代碼塊。是以并沒有什麼神奇的魔法,也沒什麼隐藏的底層機制(這裡指的是友善了解的底層)讓你可以帶着疑惑去使用它。
總結
我這個人學習方法,總結起來就是看到新技術,先在自己的代碼裡跑一遍,能跑通,并且使用起來沒有什麼難度,就基本不會深究了(如果遇到某個熊孩子就坑了)。但是自我反思過,這樣的學習方法是很不對的,寫代碼不能不求甚解,如果想要有所突破,不想局限于碼農,一定要深入探究一下實作的機制,最起碼要保證不帶着疑惑去使用。