類與類之間的通信我們有很多種方式,iOS中有代理,通知,block,單例類等等,每種方式都有其適用的場景
假設(shè)委托者皇上
發(fā)起一個委托事件 要吃飯
,這個事件的參數(shù)是今天要吃紅燒肉,水煮魚,肉末茄子
,最終做飯這件事會被代理者實施,廚師甲做紅燒肉,廚師乙做水煮魚,廚師丙做肉末茄子
在iOS開發(fā)中面對上面這個需求,我們肯定能想到用通知模式來實現(xiàn)這個邏輯。其實更好的做法是使用多播代理模式
- 用通知的方式實現(xiàn):用大喇叭廣播:“皇上要吃飯了,并且要吃紅燒肉,水煮魚,肉末茄子”,雖然廚師甲乙丙聽到之后就會開始去做給皇上做菜,但是這廣播出去全城的人都知道了,這種消息傳遞方式會造成消息外露,不受控制;
- 用多播代理的方式實現(xiàn):皇上通過吃飯總管告訴廚師甲乙丙它要吃飯了,甲乙丙收到消息后就去給皇上做菜了,這種消息傳遞很精準(zhǔn),并且不會導(dǎo)致消息外露。
一. 為什么不用通知
通知是一種零耦合的類之間通信方式,它的優(yōu)點就是能夠完全解耦,然而除了這個優(yōu)點,通知也有不少值得吐槽的地方:
- 通知的接收范圍為全局,這可能會暴露你原本想隱藏的實現(xiàn)細(xì)節(jié),比如你封裝的SDK中發(fā)出的通知,通知參數(shù)中包含敏感信息等;
- 通知的匹配完全依賴字符串,容易出現(xiàn)問題,當(dāng)項目中大量使用通知以后難以維護,極端情況會出現(xiàn)通知名重復(fù)的問題;
- 相對于代理方式,通知不能像代理一樣使用協(xié)議來
約束
代理者的方法實現(xiàn); - 通知攜帶的參數(shù)不能直觀的表達出來,依靠字典操作也增加的出錯的可能性,通知不能像代理方法那樣有返回值;
- 通知參數(shù)傳遞對于基本類型需要
裝箱
和拆箱
操作,不能傳遞nil參數(shù); - 通知有時候會打破
高內(nèi)聚低耦合
中的高內(nèi)聚
的原則,對于原本就有單向依賴的2個類來說,他們是有內(nèi)聚耦合關(guān)系的,使用通知反而將這種內(nèi)聚關(guān)系打散了,并且不利于方法調(diào)試;
二. 多播代理的思想
在C#語言中就有這樣一個概念叫做多播委托,它直接是針對對象的某個委托事件的代理,委托對象內(nèi)部保存了所有代理實現(xiàn)(指針),構(gòu)成一個委托鏈,當(dāng)這個委托事件觸發(fā)的時候這個委托鏈上的所有實現(xiàn)方法都將被調(diào)用。iOS中的多代理概念雷同,其實就是委托對象中保持多個代理對象的引用,當(dāng)觸發(fā)事件的時候,讓所有的代理對象調(diào)用相應(yīng)的代理方法即可。
三. OC中構(gòu)造多播代理
-
1.存儲多個代理
遵循iOS常規(guī)代理的實現(xiàn),我們需要一 個能夠保存多個對象弱引用
的結(jié)構(gòu),iOS中可以用多種方式實現(xiàn),這里我推薦使用NSHashTable
這個容器類,它可以指定加入到其中的對象為弱引用,并且當(dāng)其中的對象被釋放以后,該對象將會被自動從容器中移除掉
NSHashTable *delegates = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory];
[delegates addObject:delegate];
-
2.遍歷多代理,執(zhí)行代理方法
當(dāng)NSHashTable中的對象釋放以后,會被從中自動移除(經(jīng)測試hashTable的count并沒有變),我們遍歷的時候就不會遍歷到該nil對象
for (id<MyDelegate> delegate in _delegates) {
if ([delegate respondsToSelector:@selector(receiveMessage:)]) {
[delegate receiveMessage:@"a new message"];
}
}
-
3.設(shè)置(添加)代理
對于多代理我們只能用添加的方式,不能用直接賦值的方式
MyService *servie = [MyService new];
[servie addDelegate:self];
四. 簡化多代理調(diào)用
上面實現(xiàn)的多代理調(diào)用出的四行代碼都必不可少,如果一個類中有很多出代理方法的調(diào)用,那么我們就不得不寫很多這樣的代碼,沒得商量,這點必須要改進。改進方式有很多,使用方法轉(zhuǎn)發(fā)應(yīng)該是比較理想的方式
-
1.觸發(fā)方法轉(zhuǎn)發(fā)
[((id<MyDelegate>)self) receiveMessage:@"a new message"];
說明:這里self是指委托類,因為self本身沒有遵循MyDelegate協(xié)議,所有如果需要調(diào)用receiveMessage方法就先把它強制轉(zhuǎn)換為代理類型,調(diào)用方法后,self類中必然找不到receiveMessage方法,于是就會進入到方法轉(zhuǎn)發(fā)流程,最終調(diào)用代理對象的方法。也許你會說這里可以繼承協(xié)議然后調(diào)用處就不用這樣麻煩的類型轉(zhuǎn)換了,但是有一點你需要想到,如果協(xié)議中包含了
@required
修飾的方法,我們就必須實現(xiàn)它了,否則編譯器會爆出警告;
-
2.重寫方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
for (id delegate in _delegates) {
if ([delegate respondsToSelector:aSelector]) {
NSMethodSignature *result = [delegate methodSignatureForSelector:aSelector];
if (result) {
return result;
}
}
}
return [super methodSignatureForSelector:aSelector];
}
說明:方法簽名只是用來表示方法的參數(shù)個數(shù),參數(shù)類型,和返回值類型的作用,所有的代理對象實現(xiàn)的同名代理方法簽名都一樣,遍歷找到立即返回即可
-
3.重寫轉(zhuǎn)發(fā)方法
// 方法轉(zhuǎn)發(fā)
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL selector = invocation.selector;
for (id delegate in _delegates) {
if ([delegate respondsToSelector:selector]) {
invocation.target = delegate;
[invocation invoke];
}
}
}
說明:這里的invocation的target的值是當(dāng)前類實例對象(委托者),我們需要把這個值替換為delegate(代理者),意思就是讓delegate去執(zhí)行該方法;
-
五. 最佳實踐
第四節(jié)中我們在當(dāng)前的委托類中通過調(diào)用自身并不存在的方法觸發(fā)了方法轉(zhuǎn)發(fā),實現(xiàn)了封裝遍歷多代理調(diào)用代理方法的目的,但是這種方式有以下問題:
- 如果你有多個類都需要實現(xiàn)這樣的多代理模式,那么這些類中都比不可少的需要包含上述重復(fù)的代碼
- 如果該類中有一個方法和代理協(xié)議中定義的方法同名,那么我們的方法轉(zhuǎn)發(fā)也就不能進行了,進而導(dǎo)致多代理調(diào)用無法執(zhí)行
思考:我們需要一個專門的類來處理這些多代理的事情,所有需要多代理功能的類只要包含這個類的實例對象就可以了,我們把添加代理,觸發(fā)調(diào)用多代理的代碼實現(xiàn)都封裝到這個類中即可(開源框架XMPPFramework中也是類似的實現(xiàn))
-
1. 定義多代理轉(zhuǎn)發(fā)類
這個類用來封裝多代理實現(xiàn),我們使用NSProxy子類來實現(xiàn)它
@interface EEMultiProxy : NSProxy
// 代理轉(zhuǎn)發(fā)對象 工廠方法
+ (EEMultiProxy *)proxy;
// 添加代理對象
- (void)addDelegate:(id)delegate;
// 移除代理對象
- (void)removeDelete:(id)delegate;
@end
-
2. 處理多線程同步問題
為了適應(yīng)多線程環(huán)境下的多代理調(diào)用,我們在EEMultiProxy中使用信號量去解決多線程集合對象的同步問題
// 由于NSProxy類沒有init方法,所以對實例對象的初始化我們放在alloc方法中
+ (id)alloc {
EEMultiProxy *instance = [super alloc];
if (instance) {
instance->_semaphore = dispatch_semaphore_create(1);
instance->_delegates = [NSHashTable weakObjectsHashTable];
}
return instance;
}
- (void)addDelegate:(id)delegate {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
[_delegates addObject:delegate];
dispatch_semaphore_signal(_semaphore);
}
- (void)removeDelete:(id)delegate {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
[_delegates removeObject:delegate];
dispatch_semaphore_signal(_semaphore);
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSMethodSignature *methodSignature;
for (id delegate in _delegates) {
if ([delegate respondsToSelector:selector]) {
methodSignature = [delegate methodSignatureForSelector:selector];
break;
}
}
dispatch_semaphore_signal(_semaphore);
if (methodSignature) return methodSignature;
// Avoid crash, must return a methodSignature "- (void)method"
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
-
3. 異步調(diào)用多代理方法
重點1 - 多線程:每個代理類的對代理方法的實現(xiàn)都不一樣,為了使這些代理類都能及時的響應(yīng)代理調(diào)用,我們應(yīng)該將代理方法的調(diào)用都放到異步線程中;
重點2 - 遞歸死鎖:如果項目的多代理調(diào)用不采用異步派發(fā),那么就有可能因為信號量的遞歸獲取導(dǎo)致死鎖。具體表現(xiàn):代理協(xié)議實現(xiàn)類中的方法邏輯中又調(diào)用多代理proxy的方法對應(yīng)方法,這就形成了在當(dāng)前信號量中繼續(xù)嘗試獲取當(dāng)前信號量,造成信號量的遞歸等待從而形成死鎖,所以如果我們使用同步調(diào)用代理對象方法,那么我們應(yīng)該在遍歷代理集合時先拷貝一份代理集合,及時釋放信號量,然后再去遍歷調(diào)用代理方法;
- (void)forwardInvocation:(NSInvocation *)invocation {
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSHashTable *copyDelegates = [_delegates copy];
dispatch_semaphore_signal(_semaphore);
SEL selector = invocation.selector;
for (id delegate in copyDelegates) {
if ([delegate respondsToSelector:selector]) {
// must use duplicated invocation when you invoke with async
NSInvocation *dupInvocation = [self duplicateInvocation:invocation];
dupInvocation.target = delegate;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[dupInvocation invoke];
});
}
}
}
-
4. 復(fù)制invocation
因為invocation對象只有一個,每個delegate去調(diào)用的時候都會去設(shè)置invocation的target,因為我們是異步調(diào)用,有可能造成某個delegate對象的invocation調(diào)用前target被其他線程意外替換掉,很可能造成crash,所以這里需要對invocation進行復(fù)制,用來隔離每個異步調(diào)用;
- (NSInvocation *)duplicateInvocation:(NSInvocation *)invocation {
SEL selector = invocation.selector;
NSMethodSignature *methodSignature = invocation.methodSignature;
NSInvocation *dupInvocation = [NSInvocation invocationWithMethodSignature:methodSignature];
dupInvocation.selector = selector;
NSUInteger count = methodSignature.numberOfArguments;
for (NSUInteger i = 2; i < count; i++) {
void *value;
[invocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
[dupInvocation retainArguments];
return dupInvocation;
}
Demo示例鏈接:EEMultiDelegate
說明:本文中的多代理實現(xiàn)參考了框架XMPPFramework中的多代理實現(xiàn)