iOS如何實現多代理模式--OC
OC 如何實現多代理模式
為什麼要使用多代理模式
標題雖然是如何實現多代理模式,但是知道為什麼需要實現多代理模式同樣重要。
眾所周知,OC的常用的訊息傳遞方式有很多種,各有各的好處,在不同的場景選擇不同實現方式。如:
-
代理 1對1,高耦合
-
通知 1對多,鬆耦合
-
block
-
KVO
...
不同的實現方式有不同的應用場景,也有各自的優缺點。普通的代理模式只能應用與1對1的場景,針對1對多的場景只能被迫選擇使用通知。
但是通知也有自己的缺點:
-
在編譯期間不會檢查通知是否能夠被觀察者正確的處理;
-
在釋放通知的觀察者時,需要在通知中心移除觀察者;
-
在除錯的時候,通知傳遞的過程很難控制和跟蹤;
-
傳送通知和接收通知時需要提前知道通知名稱,如果通知名稱不一致,會出現不同步的情況;
-
通知發出後,不能從觀察者獲得任何的反饋資訊。對於需要返回值的場景沒有辦法處理。
如果代理模式能支援多個響應物件,那麼就不會再有以上的問題。
如何實現多代理模式
單代理模式
一個最普通的代理模式如下:
代理協議ReportDelegate:
@protocol ReportDelegate <NSObject>
//上交報告
- (void)report;
@end
類ComandA,傳送命令者
#import "ReportDelegate.h"
@interface ComandA : NSObject
@property (weak, nonatomic) id <ReportDelegate> delegate;
/**
傳送上交報告的命令
*/
- (void)sendOrder;
@end
@implementation ComandA
- (void)sendOrder
{
if(self.delegate && [self.delegate respondsToSelector:@selector(report)])
{
[self.delegate report];
}
}
@end
類ExecutorB,執行命令者
#import "ReportDelegate.h"
@interface ExecutorB : NSObject <ReportDelegate>
@end
@implementation ExecutorB
- (void)report
{
NSLog(@"我要上交報告");
}
@end
現在一個ComandA
物件A可以命令一個ExecutorB
物件B上交報告。因為ComandA
只定義了一個單成員@property (weak, nonatomic) id <ReportDelegate> delegate;
最初的多代理模式
如果現在將delegate
變為id <ReportDelegate> delegate
的陣列delegates
。在sendOrder
方法中遍歷delegates
陣列去呼叫每個delegate
執行代理方法不就好了。類似下面:
@interface ComandA : NSObject
@property (strong, nonatomic) NSPointerArray *delegates;
/**
傳送上交報告的命令
*/
- (void)sendOrder;
@end
@implementation ComandA2
- (void)sendOrder
{
for (NSUInteger i = 0; i < self.delegates.count; i += 1) {
id delegate = (__bridge id)[self.delegates pointerAtIndex:i];
if(delegate && [delegate respondsToSelector:@selector(report)])
{
[delegate report];
}
}
}
@end
多代理就這樣實現了,現在一個ComandA
物件A可以命令多個ExecutorB
物件B上交報告,只要提前將多個ExecutorB
物件加入到delegates
陣列中即可。 之所以選擇NSPointerArray
,是因為NSPointerArray
不增加成員的引用計數,相當於弱引用,在釋放一個delegate
前,就算不將其從delegates
陣列中移除也不會有問題。
一切看起來非常完美,對的,只是看起來非常完美。再深入的思考或實踐一下,就會發現這個方式運用起來多麼麻煩,哪怕更多的優化也不可避免。有興趣的可以下載這個。
pod 'MultiDelegateOC', '0.0.1'
代理協議中的每個方法都要主動遍歷呼叫每個代理物件。我們自己新建的類還好,如果我們需要將第三方庫的類變為多代理,想想那麼多的代理方法需要改動。倘若第三方庫的類新增了部分代理方法,我們也要相應的新增。
如果不想修改第三方庫的程式碼,怎麼辦,難道要在外面再封裝一層嗎?想想以後的維護工作就讓人頭疼。
進階的多代理
於是尋找進階的多代理方式已不得不做,幸好萬能的github有很多大牛,我們只需要站在他們的肩膀上就好了。
最初的多代理模式之所以有上述的問題,是因為我們讓ComandA
直接管理delegates
陣列,這樣必然會對原有程式碼進行改動。
如果我們新建一個類MultiDelegateOC
代替ComandA
管理delegates
陣列,只需要將ComandA
的@property (weak, nonatomic) id <ReportDelegate> delegate
設定為MultiDelegateOC
物件不就好了。
這樣原來的ComandA
不需要任何改動就能繼續使用多代理了。我們只需要在MultiDelegateOC
內部實現遍歷呼叫就好了。
如果我們理解OC的方法執行——訊息轉發機制就很容易實現了。我們只需要截獲MultiDelegateOC
的方法執行,將其變為遍歷執行就可以了。
這裡需要重寫NSObject
的兩個方法methodSignatureForSelector:
和forwardInvocation:
。
methodSignatureForSelector:
原型:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
這個函式讓過載方有機會丟擲一個函式的簽名,再由後面的forwardInvocation:去執行。如果當前類沒有實現這個函式導致返回值為nil,程式就會crash--未實現的函式。
forwardInvocation:
原型:
- (void)forwardInvocation:(NSInvocation *)anInvocation
函式的真正執行者,在這個方法中,我們可以從NSInvocation物件中截獲selector,引數,可以設定selector的呼叫者,真正的遍歷delegates
陣列去執行就完全沒有問題了。
//重寫respondsToSelector方法,讓`ComandA`類真實判斷。
- (BOOL)respondsToSelector:(SEL)selector
{
if ([super respondsToSelector:selector])
{
return YES;
}
for (id delegate in self.delegates)
{
if (delegate && [delegate respondsToSelector:selector])
{
return YES;
}
}
return NO;
}
//防止崩潰,生成函式簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (signature)
{
return signature;
}
[self.delegates compact];
if (self.silentWhenEmpty && self.delegates.count == 0)
{
// return any method signature, it doesn't really matter
return [self methodSignatureForSelector:@selector(description)];
}
for (id delegate in self.delegates)
{
if (!delegate)
{
continue;
}
signature = [delegate methodSignatureForSelector:selector];
if (signature)
{
break;
}
}
return signature;
}
//遍歷`delegates`陣列呼叫代理方法
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL selector = [invocation selector];
BOOL responded = NO;
NSArray *copiedDelegates = [self.delegates copy];
for (id delegate in copiedDelegates)
{
if (delegate && [delegate respondsToSelector:selector])
{
[invocation invokeWithTarget:delegate];
responded = YES;
}
}
if (!responded && !self.silentWhenEmpty)
{
[self doesNotRecognizeSelector:selector];
}
}
一個進階版的多代理模式就完成,現在我們只需要主動生成一個MultiDelegateOC
物件管理多代理就可以了。
有興趣的可以下載這個。
pod 'MultiDelegateOC', '0.0.2'
不過現在還不是很完美,如果代理協議中有返回值的情況,我們並沒有處理。再給- (void)forwardInvocation:
方法添點料就好了:
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL selector = [invocation selector];
BOOL responded = NO;
NSArray *copiedDelegates = [self.delegates copy];
void *returnValue = NULL;
for (id delegate in copiedDelegates)
{
if (delegate && [delegate respondsToSelector:selector])
{
[invocation invokeWithTarget:delegate];
if(invocation.methodSignature.methodReturnLength != 0)
{
void *value = nil;
[invocation getReturnValue:&value];
if(value)
{
returnValue = value;
}
}
responded = YES;
}
}
if(returnValue)
{
[invocation setReturnValue:&returnValue];
}
if (!responded && !self.silentWhenEmpty)
{
[self doesNotRecognizeSelector:selector];
}
}
如果多個代理物件都有返回值,最終返回將是最後加入的代理的返回值。當然NSPointerArray可以調整成員的順序,你也可以自己設定判斷條件來選擇返回值。
總結
以上是我自己在實現多代理模式的歷程,很多方法都是使用網路上大神的成熟經驗,在不斷的使用實踐踩坑中,逐步完善出來。
包括最初的多代理模式,我也使用了很長時間,後來實在覺得太麻煩。逼不得已從網上找到第二種方式,覺得挺好用。
後來測試發現總會出現莫名其妙的異常,一時之間找不到原因,不得已又切換到了第一種方式。
後來閒的時候靈光一閃,發現第二種方式有異常的情況都是在代理方法有返回值的情況下出現。知道問題原因,解決起來就簡單多了。
現在我一直使用第二種代理模式,至今沒有出現過問題。
多代理模式我一般是與單例配合使用。使用多代理的地方還不少,比如高德地圖SDK。
Demo地址
Pod引用
pod 'MultiDelegateOC'
作者:FlameGrace
連結:https://www.jianshu.com/p/fed580fa45eb
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。