[面試題]iOS多播代理

類與類之間的通信我們有很多種方式,iOS中有代理,通知,block,單例類等等,每種方式都有其適用的場景

假設委托者皇上發(fā)起一個委托事件 要吃飯,這個事件的參數(shù)是今天要吃紅燒肉,水煮魚,肉末茄子,最終做飯這件事會被代理者實施,廚師甲做紅燒肉,廚師乙做水煮魚,廚師丙做肉末茄子

在iOS開發(fā)中面對上面這個需求,我們肯定能想到用通知模式來實現(xiàn)這個邏輯。其實更好的做法是使用多播代理模式

  • 用通知的方式實現(xiàn):用大喇叭廣播:“皇上要吃飯了,并且要吃紅燒肉,水煮魚,肉末茄子”,雖然廚師甲乙丙聽到之后就會開始去做給皇上做菜,但是這廣播出去全城的人都知道了,這種消息傳遞方式會造成消息外露,不受控制;
  • 用多播代理的方式實現(xiàn):皇上通過吃飯總管告訴廚師甲乙丙它要吃飯了,甲乙丙收到消息后就去給皇上做菜了,這種消息傳遞很精準,并且不會導致消息外露。

一. 為什么不用通知

通知是一種零耦合的類之間通信方式,它的優(yōu)點就是能夠完全解耦,然而除了這個優(yōu)點,通知也有不少值得吐槽的地方:

  • 通知的接收范圍為全局,這可能會暴露你原本想隱藏的實現(xiàn)細節(jié),比如你封裝的SDK中發(fā)出的通知,通知參數(shù)中包含敏感信息等;
  • 通知的匹配完全依賴字符串,容易出現(xiàn)問題,當項目中大量使用通知以后難以維護,極端情況會出現(xiàn)通知名重復的問題;
  • 相對于代理方式,通知不能像代理一樣使用協(xié)議來約束代理者的方法實現(xiàn);
  • 通知攜帶的參數(shù)不能直觀的表達出來,依靠字典操作也增加的出錯的可能性,通知不能像代理方法那樣有返回值;
  • 通知參數(shù)傳遞對于基本類型需要裝箱拆箱操作,不能傳遞nil參數(shù);
  • 通知有時候會打破高內聚低耦合中的高內聚的原則,對于原本就有單向依賴的2個類來說,他們是有內聚耦合關系的,使用通知反而將這種內聚關系打散了,并且不利于方法調試;

二. 多播代理的思想

在C#語言中就有這樣一個概念叫做多播委托,它直接是針對對象的某個委托事件的代理,委托對象內部保存了所有代理實現(xiàn)(指針),構成一個委托鏈,當這個委托事件觸發(fā)的時候這個委托鏈上的所有實現(xiàn)方法都將被調用。iOS中的多代理概念雷同,其實就是委托對象中保持多個代理對象的引用,當觸發(fā)事件的時候,讓所有的代理對象調用相應的代理方法即可。

多播代理.png

三. OC中構造多播代理

  • 1.存儲多個代理

遵循iOS常規(guī)代理的實現(xiàn),我們需要一 個能夠保存多個對象弱引用的結構,iOS中可以用多種方式實現(xiàn),這里我推薦使用NSHashTable這個容器類,它可以指定加入到其中的對象為弱引用,并且當其中的對象被釋放以后,該對象將會被自動從容器中移除掉

    NSHashTable *delegates = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory];
    [delegates addObject:delegate];
  • 2.遍歷多代理,執(zhí)行代理方法

當NSHashTable中的對象釋放以后,會被從中自動移除(經測試hashTable的count并沒有變),我們遍歷的時候就不會遍歷到該nil對象

  for (id<MyDelegate> delegate in _delegates) {
        if ([delegate respondsToSelector:@selector(receiveMessage:)]) {
            [delegate receiveMessage:@"a new message"];
        }
    }
  • 3.設置(添加)代理

對于多代理我們只能用添加的方式,不能用直接賦值的方式

MyService *servie = [MyService new];
[servie addDelegate:self];

四. 簡化多代理調用

上面實現(xiàn)的多代理調用出的四行代碼都必不可少,如果一個類中有很多出代理方法的調用,那么我們就不得不寫很多這樣的代碼,沒得商量,這點必須要改進。改進方式有很多,使用方法轉發(fā)應該是比較理想的方式

  • 1.觸發(fā)方法轉發(fā)
[((id<MyDelegate>)self) receiveMessage:@"a new message"];

說明:這里self是指委托類,因為self本身沒有遵循MyDelegate協(xié)議,所有如果需要調用receiveMessage方法就先把它強制轉換為代理類型,調用方法后,self類中必然找不到receiveMessage方法,于是就會進入到方法轉發(fā)流程,最終調用代理對象的方法。也許你會說這里可以繼承協(xié)議然后調用處就不用這樣麻煩的類型轉換了,但是有一點你需要想到,如果協(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.重寫轉發(fā)方法
// 方法轉發(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的值是當前類實例對象(委托者),我們需要把這個值替換為delegate(代理者),意思就是讓delegate去執(zhí)行該方法;

  • 五. 最佳實踐

第四節(jié)中我們在當前的委托類中通過調用自身并不存在的方法觸發(fā)了方法轉發(fā),實現(xiàn)了封裝遍歷多代理調用代理方法的目的,但是這種方式有以下問題:

  • 如果你有多個類都需要實現(xiàn)這樣的多代理模式,那么這些類中都比不可少的需要包含上述重復的代碼
  • 如果該類中有一個方法和代理協(xié)議中定義的方法同名,那么我們的方法轉發(fā)也就不能進行了,進而導致多代理調用無法執(zhí)行

思考:我們需要一個專門的類來處理這些多代理的事情,所有需要多代理功能的類只要包含這個類的實例對象就可以了,我們把添加代理,觸發(fā)調用多代理的代碼實現(xiàn)都封裝到這個類中即可(開源框架XMPPFramework中也是類似的實現(xiàn))

  • 1. 定義多代理轉發(fā)類

這個類用來封裝多代理實現(xiàn),我們使用NSProxy子類來實現(xiàn)它

@interface EEMultiProxy : NSProxy
// 代理轉發(fā)對象 工廠方法
+ (EEMultiProxy *)proxy;
// 添加代理對象
- (void)addDelegate:(id)delegate;
// 移除代理對象
- (void)removeDelete:(id)delegate;

@end
  • 2. 處理多線程同步問題

為了適應多線程環(huán)境下的多代理調用,我們在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. 異步調用多代理方法

重點1 - 多線程:每個代理類的對代理方法的實現(xiàn)都不一樣,為了使這些代理類都能及時的響應代理調用,我們應該將代理方法的調用都放到異步線程中;
重點2 - 遞歸死鎖:如果項目的多代理調用不采用異步派發(fā),那么就有可能因為信號量的遞歸獲取導致死鎖。具體表現(xiàn):代理協(xié)議實現(xiàn)類中的方法邏輯中又調用多代理proxy的方法對應方法,這就形成了在當前信號量中繼續(xù)嘗試獲取當前信號量,造成信號量的遞歸等待從而形成死鎖,所以如果我們使用同步調用代理對象方法,那么我們應該在遍歷代理集合時先拷貝一份代理集合,及時釋放信號量,然后再去遍歷調用代理方法;

- (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. 復制invocation

因為invocation對象只有一個,每個delegate去調用的時候都會去設置invocation的target,因為我們是異步調用,有可能造成某個delegate對象的invocation調用前target被其他線程意外替換掉,很可能造成crash,所以這里需要對invocation進行復制,用來隔離每個異步調用;

- (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)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

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

友情鏈接更多精彩內容