1.block和代理的對比
2.雙方的優缺點
3.什麼樣的環境下,優先使用哪一種?依據是什麼?
block 和 delegate 都可以通知外面。block 更輕型,使用更簡單,能夠直接訪問上下文,這樣類中不需要存儲臨時數據,使用 block 的代碼通常會在同一個地方,這樣讀代碼也連貫。delegate 更重一些,需要實現接口,它的方法分離開來,很多時候需要存儲一些臨時數據,另外相關的代碼會被分離到各處,沒有 block 好讀。
應該優先使用 block。而有兩個情況可以考慮 delegate。
1. 有多個相關方法。假如每個方法都設置一個 block, 這樣會更麻煩。而 delegate 讓多個方法分成一組,只需要設置一次,就可以多次回調。當多於 3 個方法時就應該優先采用 delegate。
比如一個網絡類,假如只有成功和失敗兩種情況,每個方法可以設計成單獨 block。但假如存在多個方法,比如有成功、失敗、緩存、https 驗證,網絡進度等等,這種情況下,delegate 就要比 block 要好。
在 swift 中,利用 enum, 多個方法也可以合並成一個 block 接口。swift 中的枚舉根據情況不同,可以關聯不同數據類型。而在 objc 就不建議這樣做,objc 這種情況下,額外數據需要使用 NSObject 或者 字典進行強轉,接口就不夠安全。
2. 為了避免循環引用,也可以使用 delegate。使用 block 時稍微不注意就形成循環引用,導致對象釋放不了。這種循環引用,一旦出現就比較難檢查出來。而 delegate 的方法是分離開的,並不會引用上下文,因此會更安全些。
假如寫一個庫供他人使用,不清楚使用者的水平如何。這時為防止誤用,寧願麻煩一些,笨一些,使用 delegate 來替代 block。
將 block 簡單分類,有三種情形。
* 臨時性的,只用在棧當中,不會存儲起來。
比如數組的 foreach 遍歷,這個遍歷用到的 block 是臨時的,不會存儲起來。
* 需要存儲起來,但只會調用一次,或者有一個完成時期。
比如一個 UIView 的動畫,動畫完成之後,需要使用 block 通知外面,一旦調用 block 之後,這個 block 就可以刪掉。
* 需要存儲起來,可能會調用多次。
比如按鈕的點擊事件,假如采用 block 實現,這種 block 就需要長期存儲,並且會調用多次。調用之後,block 也不可以刪除,可能還有下一次按鈕的點擊。
對於臨時性的,只在棧中使用的 block, 沒有循環引用問題,block 會自動釋放。而只調用一次的 block,需要看內部的實現,正確的實現應該是 block 調用之後,馬上賦值為空,這樣 block 也會釋放,同樣不會循環引用。
而多次調用時,block 需要長期存儲,就很容易出現循環引用問題。
Cocoa 中的 API 設計也是這樣的,臨時性的,只會調用一次的,采用 block。而多次調用的,並不會使用 block。比如按鈕事件,就使用 target-action。有些庫將按鈕事件從 target-action 封裝成 block 接口, 反而容易出問題。
Block 和 Delegate 兩種在 iOS 開發中都有廣泛的應用,如果加上 NSNotification 的話就把三大 iOS 回調方式給湊齊了。我這裡根據這三者來進行說明。
Delegation (代理)是一種很常見的回調方式,和 Singleton (一樣),在大部分編程語言裡都有實現方法。
Block 是一種蘋果開發的基於 C 的調用方式 [1],在 iOS 和 Mac 開發上都有廣泛的應用。Block 從某種程度上來說是一種很新的回調方式,蘋果在2009年將其引入 Mac OS X 10.6,之後在2010年將其引入到 iOS 4.0。值得一提的是,同樣是在 iOS 4.0 ,蘋果引入了 ARC (Automatic Reference Counting),准確的說的話是 ARCLite , iOS 5 才正式加入了 ARC [2]。
NSNotification 這個作為蘋果在 2001 年發布 Mac OS X 的時候就集成在 Foundation 庫裡面的類,一直作為一種全局回調通知的功能而存在。有趣的是,按照蘋果官方文檔對於 Delegation 的說明,在 Cocoa Framwroks 中,蘋果的 Delegation 被表述為是一種單觀察者通知的實現。[3]
三種回調方式,能共存於 Objective-C 之中,現在又集體被繼承到了 Swift 裡面,說明三者肯定是有不同和各自的優劣之處的。
下面我就我個人這些年的 iOS 開發了解,簡單說一下三者的優劣。
Delegation 是一種很清晰回調形式,從 Protocol 的建立,到之後的引用,和對於 delegate 聲明的變量處理,都非常具有條理。建立完 Delegation 之後,其他方法在調用的時候會很清晰整個回調的流程和處理。在處理一些延遲回調或者觸發回掉的時候,聲明調用的類裡面的回調方法在編寫時可以按照很獨立的邏輯在制作。在使用 Delegation 的時候,一個回調方法可以對應多個不同的 Delegation ,這不僅簡化了編程過程,也讓能回調處理更加清晰。
Delegation 不好的地方在於,在類中調用 delegate 方法的時候,需要對 delegate 本體進行一定的驗證核對,防止出現方法對象為空的情況,再一個就是受制於 performSelector 這個 selector 方法,回掉返回的參數被限制在了 NS 類的范圍內,數量也很有限(當然可以用直接調用方法的形式在繞過,並不推薦;也可以用 Array 套著傳, 不過這樣需要有文檔支持,不然不夠清晰,回調方法也需要獨立的驗證,故也不推薦):
if (self.delegate && [self.delegate respondsToSelector:@selector(ABC:)]){
[self.delegate performSelector:@selector(ABC:) withObject:@"ABC"];
}
//需要驗證代理是否為空,同時驗證代理方法是否存在
這個問題在 Swfit 中有了很不錯的解決:
delegate?.ABC?(@"ABC")
至於說 Delegation 的另外一個問題,那就是使用了 Delegation 之後,代碼閱讀變成了一件很困難的事情,你需要在不同的 Class 文件中一次次的跳轉來理解整個代碼思路,要知道糟糕的 XCode 還經常會跳錯方法(Command + 點擊跳轉,跳到了同名的其他方法),導致代碼可讀性的下降。要說的話,這一點在 Swift(2.2)版本中有一定的優化,不過 Swift 的 Selector 還在修正中,之後也許會在對於 Delegation 的可讀性上做更多的優化。
最後一個比較核心的問題就在於,在一批變量聲明了代理之後,在代理回掉被執行時,你是不太好知道這個變量究竟是這一批中的哪一個的。這也就是為什麼蘋果在制作 delgate 和 datasource 的時候都會在代理方法的返回值中帶上聲明代理類本身的原因。
Block 是一種很好用的回掉方式,其解決了 Delegation 在確認聲明對象上的問題,由於 Block 回調方法一般都跟隨在聲明之後,所以可以很快確認回調來源,通過 __block 聲明的變量也可以很方便的穿越到回調方法中進行使用,代碼可讀性上,一般來說使用 Block 的代碼比使用 Delegation 的代碼可讀性更強(當然你也可以聲明 Block 回調方法對應變量,然後按照和 Delegation 差不多的方法來調用他)。
Block 的缺陷主要就在於使用 ARC 情況下的循環引用。從某種程度上來說, Block 回調的方法內變量實際上是關聯在聲明 Block 的類裡面的,但 Block 回調方法本身是在使用 Block 的類中的,同時使用類的 self 本身是可以在 Block 回調方法中被請求的,這裡就會出現使用類 A 引用聲明類 B ,B 在關聯的 Block 中又引用了使用類 A ,這樣一個循環之後引用可以無限循環下去,在這種無限循環的引用下, ARC 就無法知道 A 和 B 的確切棄用時間,所以 A 和 B 會在內存中永遠存活下去,直到進程被消滅。所以,在很多時候,我們必須要使用 __weak 來聲明一個 weakself ,來幫助 ARC 判斷內存回收的時間。
這裡要指出的是, Block 的回調方法也是可以復用,創建回調方法這一套東西在 Objective-C 中是繼承於 C 樣式的,一般來說,這個東西是沒人用。[4]
void (^simpleBlock)(id responseObject) = ^(id responseObject){
NSLog(@"This is a block");
};
//這裡創建了 simpleBlock 之後就可以像 Delegation 回調方法那樣在各種回調聲明中使用了
NSNotification 本身是一個非常強大的回調,因為他的回調是全局而且一對多的,廣播的特性使得很多時候使用 NSNotification 能一次解決大面積的問題。同時,由於 NSNotification 是一套很古老的回調機制,在一些時候,當我們需要對 iOS Framework 做一些特殊處理的時候,會用到一些獲取隱藏 NSNotification 的方法,這個在之前權限不完善的時候,企業開發中使用很廣泛。記得當年 iOS 7 Tech Talk 上海站,我帶著代碼去找蘋果的人問一個問題的時候,他們就查了之後文檔給了我一個隱藏的 NSNotification 名,幫我解決了問題。(這個通知之後開放給開發者了,記得應該是一個鍵盤的通知)
至於說 NSNotification,就和上面說到的一樣,也主要是在於他的全局廣播屬性和權限機制。
在使用 NSNotification 的時候,注冊完回調方法之後,開發者需要自行對注冊進行取消操作。比如說你注冊一個 A 方法到 B 通知,由於某些原因 A 方法所在的類被 ARC 回收掉了,那麼在 B 通知被觸發的時候,由於 A 的通知注冊信息還在,就會出現調用了非法的內存地址的情況。我曾經遇到了一次,應該是在調用 Jianting Zhu 還是 Jim Liu 的 WeiboSDK 的時候,記得那一版 SDK 特別奇葩,所有回調都是通過 NSNotification 來進行的,一開始用的時候不太懂,各種注冊回調方法,然後發現 App 跑起來會莫名的閃退,那時候也不太懂,只能硬著頭皮把代碼都改成代理的形式。後來在做鍵盤的時候碰到了類似的報錯,花時間研究了之後才發現是通知回調了已經被回收的類裡面的方法,這時候才知道要如果通知沒有注銷會帶來調用上的問題。
至於說 NSNotification 的權限問題,對於寫類的人來說是比較頭疼的,很多時候只能靠文檔和調用者的自覺,出於權限考慮,如果不是特別需求全局廣播這個特性,一般不太建議使用 NSNotification。
NSNotification 由於自身的限制,在回調可以傳遞的內容上也存在數量和內容的限制,雖然可以通過 Array 的方法繞過,但這樣在代碼可讀性就會有折扣,對於文檔也需要有要求,回調方法中還需要驗證。
那麼,在何種情況下使用上面三者呢?
對於初級的開發人員來說,關鍵在於使用習慣。如果你從其他語言轉到 Objective-C 或者 Swift ,相信 Delegation 肯定讓你覺得更加親切,那麼在初級階段請使用好這個語法糖,多用,多去理解;如果你用著 AFNetworking 看著其他老前輩的說法用 Block 覺得效率很高很開心,那就開心的用,直到你被循環引用煩到了為止(笑);如果你用 NSNotification ……你還是別用了。然後,在你代碼寫多了之後,你可以開始嘗試接觸其他回調方式,去感受這些回調方式的不同。
對於中級的開發人員,關鍵在於對於回調流程的理解。
你要知道你的回調是一個什麼性質的回調,如果這個回調是一個不定期觸發,或者會多次觸發的,那麼 Delegation 應該更適合;如果這個回調是一個一次性的,並且和調用方法是單線性關系的,那麼 Block 應該更適合;如果這個回調是廣播性質的,需要很多個不同的類都接收到,那麼 NSNotification 更適合。
在不同的執行線(不是線程),不同的執行次數、執行數量上的區別,是鑒別使用哪一種回調的最好判斷方法。
對於 Block 來說,他的執行線應該是和調用方法、回調方法連續在一起的;對於 Delegation 和 NSNotification 來說,他的執行線可以是連續的,也可以是調用方法和回調方法之間有很長的間隔,或者說回調方法在執行線上會多次出現。
對於高級的開發人員……你們要是不懂上面那些就不要做高級開發了,你們應該去研究線程、 GCD 、 NSOperation 這些玩意。