[面試題]iOS多播代理

類與類之間的通信我們有很多種方式叹螟,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)的代理方法即可抬旺。

多播代理.png

三. 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)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市笤成,隨后出現(xiàn)的幾起案子评架,更是在濱河造成了極大的恐慌,老刑警劉巖炕泳,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件古程,死亡現(xiàn)場離奇詭異,居然都是意外死亡喊崖,警方通過查閱死者的電腦和手機(jī)挣磨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來荤懂,“玉大人茁裙,你說我怎么就攤上這事〗诜拢” “怎么了晤锥?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我矾瘾,道長女轿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任壕翩,我火速辦了婚禮蛉迹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘放妈。我一直安慰自己北救,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布芜抒。 她就那樣靜靜地躺著珍策,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宅倒。 梳的紋絲不亂的頭發(fā)上攘宙,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音拐迁,去河邊找鬼模聋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛唠亚,可吹牛的內(nèi)容都是我干的链方。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼灶搜,長吁一口氣:“原來是場噩夢啊……” “哼祟蚀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起割卖,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤前酿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后鹏溯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罢维,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年丙挽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肺孵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡颜阐,死狀恐怖平窘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凳怨,我是刑警寧澤瑰艘,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布是鬼,位于F島的核電站,受9級特大地震影響紫新,放射性物質(zhì)發(fā)生泄漏均蜜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一芒率、第九天 我趴在偏房一處隱蔽的房頂上張望囤耳。 院中可真熱鬧,春花似錦敲董、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至化焕,卻和暖如春萄窜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背撒桨。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工查刻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凤类。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓穗泵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谜疤。 傳聞我的和親對象是個(gè)殘疾皇子佃延,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法夷磕,內(nèi)部類的語法履肃,繼承相關(guān)的語法,異常的語法坐桩,線程的語...
    子非魚_t_閱讀 31,587評論 18 399
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,125評論 29 470
  • 把網(wǎng)上的一些結(jié)合自己面試時(shí)遇到的面試題總結(jié)了一下尺棋,以后有新的還會(huì)再加進(jìn)來。 1. OC 的理解與特性 OC 作為一...
    AlaricMurray閱讀 2,546評論 0 20
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉绵跷,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,682評論 0 9
  • 1.Switch能否用String膘螟? 在java7之前,Switch值能支持int碾局,byte萍鲸,short,char...
    小莊bb閱讀 668評論 0 0