類與類之間的通信我們有很多種方式叹螟,iOS中有代理罕袋,通知,block亮蛔,單例類等等痴施,每種方式都有其適用的場景
假設(shè)委托者皇上
發(fā)起一個(gè)委托事件 要吃飯
,這個(gè)事件的參數(shù)是今天要吃紅燒肉究流,水煮魚辣吃,肉末茄子
,最終做飯這件事會(huì)被代理者實(shí)施芬探,廚師甲做紅燒肉神得,廚師乙做水煮魚,廚師丙做肉末茄子
在iOS開發(fā)中面對上面這個(gè)需求偷仿,我們肯定能想到用通知模式來實(shí)現(xiàn)這個(gè)邏輯哩簿。其實(shí)更好的做法是使用多播代理模式
- 用通知的方式實(shí)現(xiàn):用大喇叭廣播:“皇上要吃飯了,并且要吃紅燒肉炎疆,水煮魚卡骂,肉末茄子”国裳,雖然廚師甲乙丙聽到之后就會(huì)開始去做給皇上做菜形入,但是這廣播出去全城的人都知道了,這種消息傳遞方式會(huì)造成消息外露缝左,不受控制亿遂;
- 用多播代理的方式實(shí)現(xiàn):皇上通過吃飯總管告訴廚師甲乙丙它要吃飯了浓若,甲乙丙收到消息后就去給皇上做菜了,這種消息傳遞很精準(zhǔn)蛇数,并且不會(huì)導(dǎo)致消息外露挪钓。
一. 為什么不用通知
通知是一種零耦合的類之間通信方式,它的優(yōu)點(diǎn)就是能夠完全解耦耳舅,然而除了這個(gè)優(yōu)點(diǎn)碌上,通知也有不少值得吐槽的地方:
- 通知的接收范圍為全局,這可能會(huì)暴露你原本想隱藏的實(shí)現(xiàn)細(xì)節(jié)浦徊,比如你封裝的SDK中發(fā)出的通知馏予,通知參數(shù)中包含敏感信息等;
- 通知的匹配完全依賴字符串盔性,容易出現(xiàn)問題霞丧,當(dāng)項(xiàng)目中大量使用通知以后難以維護(hù),極端情況會(huì)出現(xiàn)通知名重復(fù)的問題冕香;
- 相對于代理方式蛹尝,通知不能像代理一樣使用協(xié)議來
約束
代理者的方法實(shí)現(xiàn); - 通知攜帶的參數(shù)不能直觀的表達(dá)出來悉尾,依靠字典操作也增加的出錯(cuò)的可能性突那,通知不能像代理方法那樣有返回值;
- 通知參數(shù)傳遞對于基本類型需要
裝箱
和拆箱
操作焕襟,不能傳遞nil參數(shù)陨收; - 通知有時(shí)候會(huì)打破
高內(nèi)聚低耦合
中的高內(nèi)聚
的原則,對于原本就有單向依賴的2個(gè)類來說鸵赖,他們是有內(nèi)聚耦合關(guān)系的务漩,使用通知反而將這種內(nèi)聚關(guān)系打散了,并且不利于方法調(diào)試它褪;
二. 多播代理的思想
在C#語言中就有這樣一個(gè)概念叫做多播委托饵骨,它直接是針對對象的某個(gè)委托事件的代理,委托對象內(nèi)部保存了所有代理實(shí)現(xiàn)(指針)茫打,構(gòu)成一個(gè)委托鏈居触,當(dāng)這個(gè)委托事件觸發(fā)的時(shí)候這個(gè)委托鏈上的所有實(shí)現(xiàn)方法都將被調(diào)用。iOS中的多代理概念雷同老赤,其實(shí)就是委托對象中保持多個(gè)代理對象的引用轮洋,當(dāng)觸發(fā)事件的時(shí)候,讓所有的代理對象調(diào)用相應(yīng)的代理方法即可抬旺。
三. OC中構(gòu)造多播代理
-
1.存儲(chǔ)多個(gè)代理
遵循iOS常規(guī)代理的實(shí)現(xiàn)弊予,我們需要一 個(gè)能夠保存多個(gè)對象弱引用
的結(jié)構(gòu),iOS中可以用多種方式實(shí)現(xiàn)开财,這里我推薦使用NSHashTable
這個(gè)容器類汉柒,它可以指定加入到其中的對象為弱引用误褪,并且當(dāng)其中的對象被釋放以后,該對象將會(huì)被自動(dòng)從容器中移除掉
NSHashTable *delegates = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory];
[delegates addObject:delegate];
-
2.遍歷多代理碾褂,執(zhí)行代理方法
當(dāng)NSHashTable中的對象釋放以后兽间,會(huì)被從中自動(dòng)移除(經(jīng)測試hashTable的count并沒有變),我們遍歷的時(shí)候就不會(huì)遍歷到該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)用
上面實(shí)現(xiàn)的多代理調(diào)用出的四行代碼都必不可少嘀略,如果一個(gè)類中有很多出代理方法的調(diào)用,那么我們就不得不寫很多這樣的代碼乓诽,沒得商量屎鳍,這點(diǎn)必須要改進(jìn)。改進(jìn)方式有很多问裕,使用方法轉(zhuǎn)發(fā)應(yīng)該是比較理想的方式
-
1.觸發(fā)方法轉(zhuǎn)發(fā)
[((id<MyDelegate>)self) receiveMessage:@"a new message"];
說明:這里self是指委托類逮壁,因?yàn)閟elf本身沒有遵循MyDelegate協(xié)議,所有如果需要調(diào)用receiveMessage方法就先把它強(qiáng)制轉(zhuǎn)換為代理類型粮宛,調(diào)用方法后窥淆,self類中必然找不到receiveMessage方法,于是就會(huì)進(jìn)入到方法轉(zhuǎn)發(fā)流程巍杈,最終調(diào)用代理對象的方法忧饭。也許你會(huì)說這里可以繼承協(xié)議然后調(diào)用處就不用這樣麻煩的類型轉(zhuǎn)換了,但是有一點(diǎn)你需要想到筷畦,如果協(xié)議中包含了
@required
修飾的方法词裤,我們就必須實(shí)現(xiàn)它了,否則編譯器會(huì)爆出警告鳖宾;
-
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ù)個(gè)數(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)前類實(shí)例對象(委托者),我們需要把這個(gè)值替換為delegate(代理者)拇惋,意思就是讓delegate去執(zhí)行該方法周偎;
-
五. 最佳實(shí)踐
第四節(jié)中我們在當(dāng)前的委托類中通過調(diào)用自身并不存在的方法觸發(fā)了方法轉(zhuǎn)發(fā),實(shí)現(xiàn)了封裝遍歷多代理調(diào)用代理方法的目的撑帖,但是這種方式有以下問題:
- 如果你有多個(gè)類都需要實(shí)現(xiàn)這樣的多代理模式蓉坎,那么這些類中都比不可少的需要包含上述重復(fù)的代碼
- 如果該類中有一個(gè)方法和代理協(xié)議中定義的方法同名,那么我們的方法轉(zhuǎn)發(fā)也就不能進(jìn)行了胡嘿,進(jìn)而導(dǎo)致多代理調(diào)用無法執(zhí)行
思考:我們需要一個(gè)專門的類來處理這些多代理的事情蛉艾,所有需要多代理功能的類只要包含這個(gè)類的實(shí)例對象就可以了,我們把添加代理,觸發(fā)調(diào)用多代理的代碼實(shí)現(xiàn)都封裝到這個(gè)類中即可(開源框架XMPPFramework中也是類似的實(shí)現(xiàn))
-
1. 定義多代理轉(zhuǎn)發(fā)類
這個(gè)類用來封裝多代理實(shí)現(xiàn)伺通,我們使用NSProxy子類來實(shí)現(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中使用信號(hào)量去解決多線程集合對象的同步問題
// 由于NSProxy類沒有init方法逢享,所以對實(shí)例對象的初始化我們放在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)用多代理方法
重點(diǎn)1 - 多線程:每個(gè)代理類的對代理方法的實(shí)現(xiàn)都不一樣罐监,為了使這些代理類都能及時(shí)的響應(yīng)代理調(diào)用,我們應(yīng)該將代理方法的調(diào)用都放到異步線程中瞒爬;
重點(diǎn)2 - 遞歸死鎖:如果項(xiàng)目的多代理調(diào)用不采用異步派發(fā)弓柱,那么就有可能因?yàn)樾盘?hào)量的遞歸獲取導(dǎo)致死鎖。具體表現(xiàn):代理協(xié)議實(shí)現(xiàn)類中的方法邏輯中又調(diào)用多代理proxy的方法對應(yīng)方法侧但,這就形成了在當(dāng)前信號(hào)量中繼續(xù)嘗試獲取當(dāng)前信號(hào)量矢空,造成信號(hào)量的遞歸等待從而形成死鎖,所以如果我們使用同步調(diào)用代理對象方法禀横,那么我們應(yīng)該在遍歷代理集合時(shí)先拷貝一份代理集合屁药,及時(shí)釋放信號(hào)量,然后再去遍歷調(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
因?yàn)閕nvocation對象只有一個(gè)酿箭,每個(gè)delegate去調(diào)用的時(shí)候都會(huì)去設(shè)置invocation的target,因?yàn)槲覀兪钱惒秸{(diào)用趾娃,有可能造成某個(gè)delegate對象的invocation調(diào)用前target被其他線程意外替換掉缭嫡,很可能造成crash,所以這里需要對invocation進(jìn)行復(fù)制抬闷,用來隔離每個(gè)異步調(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
說明:本文中的多代理實(shí)現(xiàn)參考了框架XMPPFramework中的多代理實(shí)現(xiàn)