[面試題]iOS多播代理

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

多播代理.png

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,436評論 2 378

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

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