元件化方案調研

西木柚子發表於2018-02-15

元件化方案調研

前言

在最前面,先祝大家除夕節快樂,開開心心過大年~~

這篇文章主要是我近段時間針對市面上存在的一些元件化方案的調研之後,再經過自己的反思和總結寫的,部落格中部分文字和圖借鑑自下面的部落格。各位看官大爺就當做一篇讀書筆記來看即可,主要是參考瞭如下幾篇文章,另外零零散散的也看了一些其他資料,但是大多都是相似的

  1. 蘑菇街元件化之路
  2. iOS應用架構談 元件化方案
  3. iOS 元件化 —— 路由設計思路分析
  4. 滴滴iOS的元件化實踐與優化
  5. iOS元件化方案
  6. iOS 元件化方案探索
  7. 掌上鍊家元件化探索歷程
  8. 京東iOS客戶端元件管理實踐

看上去各家都是各顯神通,都有自己的技術方案,但是實際上都可以歸類到如下兩種方案:

  1. 利用runtime實現的target-action方法
  2. 利用url-scheme方案

目前市面上流行的元件化方案都是通過url-scheme實現的,包括很多開源的元件化的庫都是如此,只有casa的方案獨樹一幟,是通過Target-Action實現的

URL-Scheme庫:
  1. JLRoutes
  2. routable-ios
  3. HHRouter
  4. MGJRouter
Target-Action庫:
  1. CTMediator

上面這些第三方元件庫的具體對比,大家可以參考霜神的這篇部落格:

iOS 元件化 —— 路由設計思路分析

URL-Sheme方案一般都是各個元件把自己可以提供的服務通過url的形式註冊到一箇中心管理器,然後呼叫發就可以通過openURL的方式來開啟這個url,然後中心管理器解析這個url,把請求轉發到相應的元件去執行

Target-Action方案利用了OC的runtime特性,無需註冊,直接在原有的元件之外加一層wrapper,把對外提供的服務都抽離到該層。然後通過runtime的TARGET performSelector:ACTION withObject:PARAMS找到對應的元件,執行方法和傳遞引數。

就我個人而言,我還是比較推薦target-action方案,具體原因我們下面會進一步分析

為何要元件化

在做一件事之前我們一般都要搞清楚為什麼要這麼做,好處是什麼,有哪些坑,這樣才會有一個整體的認識,然後再決定要不要做。同樣我們也要搞清楚到底需不需要實施元件化,那麼就要先搞清楚什麼是元件

元件的定義

元件是由一個或多個類構成,能完整描述一個業務場景,並能被其他業務場景複用的功能單位。元件就像是PC時代個人組裝電腦時購買的一個個部件,比如記憶體,硬碟,CPU,顯示器等,拿出其中任何一個部件都能被其他的PC所使用。

所以元件可以是個廣義上的概念,並不一定是頁面跳轉,還可以是其他不具備UI屬性的服務提供者,比如日誌服務,VOIP服務,記憶體管理服務等等。說白了我們目標是站在更高的維度去封裝功能單元。對這些功能單元進行進一步的分類,才能在具體的業務場景下做更合理的設計。

元件化的優點

縱觀目前的已經在實施元件化的團隊來看,大家的一般發展路徑都是:前期專案小,需要快速迭代搶佔市場,大家都是用傳統的MVC架構去開發專案。等到後期專案越來越大,開發人數越來越多,會發現傳統的開發方式導致程式碼管理混亂,釋出、整合、測試越來越麻煩,被迫走向元件化的道路。

其實元件化也不是完全必須的,如果你的團隊只是開發一個小專案,團隊人數小於10個人,產品線也就是兩三條,那麼完全可以用傳統開發方式來開發。但是如果你的團隊在不斷髮展,產品線也越來越多的時候,預計後期可能會更多的時候,那麼最好儘早把元件化提上議程。

摘自casa的建議:

元件化方案在App業務穩定,且規模(業務規模和開發團隊規模)增長初期去實施非常重要,它助於將複雜App分而治之,也有助於多人大型團隊的協同開發。但元件化方案不適合在業務不穩定的情況下過早實施,至少要等產品已經經過MVP階段時才適合實施元件化。因為業務不穩定意味著鏈路不穩定,在不穩定的鏈路上實施元件化會導致將來主業務產生變化時,全域性性模組排程和重構會變得相對複雜。

其實元件化也沒有多麼高大上,和我們之前說的模組化差不多,就是把一些業務、基礎功能剝離,劃分為一個個的模組,然後通過pods的方式管理而已,同時要搭配一套後臺的自動整合、釋出、測試流程

一般當專案越來越大的時候,無可避免的會遇到如下的痛點:

程式碼衝突多,編譯慢。

每一次拉下程式碼開發功能,開發完成準備提交程式碼時,往往有其他工程師提交了程式碼,需要重新拉去程式碼合併後再提交,即使開發一個很小的功能,也需要在整個工程裡做編譯和除錯,效率較低。

迭代速度慢,耦合比較嚴重,無法單獨測試。

各個業務模組之間互相引入,耦合嚴重。每次需要發版時,所有的業務線修改都需要全部迴歸,然後審檢視是否出錯,耗費大量時間。業務線之間相互依賴,可能會導致一個業務線必須等待另外一個業務線開發完某個功能才可以接著開發,無法並行開發。還有一個問題,就是耦合導致無法單獨測試某個業務線,可能需要等到所有業務線開發完畢,才能統一測試,浪費測試資源

為了解決上述痛點,元件化應運而生,總體來說,元件化就是把整個專案進行拆分,分成一個個單獨的可獨立執行的元件,分開管理,減少依賴。 完成元件化之後,一般可達到如下效果:

  1. 加快編譯速度,可以把不會經常變動的元件做成靜態庫,同時每個元件可以獨立編譯,不依賴於主工程或者其他元件
  2. 每個元件都可以選擇自己擅長的開發模式(MVC / MVVM / MVP)
  3. 可以單獨測試每個元件
  4. 多條業務線可以並行開發,提高開發效率

如何元件化

當我們確定需要對專案進行元件化了,我們第一個要解決的問題就是如何拆分元件。這是一個見仁見智的問題,沒有太明確的劃分邊界,大致做到每個元件只包含一個功能即可,具體實施還是要根據實際情況權衡。

當我們寫一個類的時候,我們會謹記高內聚,低耦合的原則去設計這個類,當涉及多個類之間互動的時候,我們也會運用SOLID原則,或者已有的設計模式去優化設計,但在實現完整的業務模組的時候,我們很容易忘記對這個模組去做設計上的思考,粒度越大,越難做出精細穩定的設計,我暫且把這個粒度認為是元件的粒度。

元件可以是個廣義上的概念,並不一定是頁面跳轉,還可以是其他不具備UI屬性的服務提供者,比如日誌服務,VOIP服務,記憶體管理服務等等。說白了我們目標是站在更高的維度去封裝功能單元,把多個功能單元組合在一起形成一個更大的功能單元,也就是元件。對這些功能單元進行進一步的分類,才能在具體的業務場景下做更合理的設計。

下面的元件劃分粒度,大家可以借鑑一下

元件化方案調研

元件化前後對比

iOS裡面的元件化主要是通過cocopods把元件打包成單獨的私有pod庫來進行管理,這樣就可以通過podfile檔案,進行動態的增刪和版本管理了。

下面是鏈家APP在實行元件化前後的對比

元件化方案調研
元件化方案調研

可以看到傳統的MVC架構把所有的模組全部糅合在一起,是一種分散式的管理方法,耦合嚴重,當業務線過多的時候就會出現我們上面說的問題。 而下圖的元件化方式是一種中心Mediator的方式,讓所有業務元件都分開,然後都依賴於Mediator進行統一管理,減少耦合。

元件化後,程式碼分類也更符合人類大腦的思考方式

元件化方案調研

元件化方案對比分析

元件化如何解決現有工程問題

傳統模式的元件之間的跳轉都是通過直接import,當模組比較少的時候這個方式看起來沒啥問題。但到了專案越來越龐大,這種模式會導致每個模組都離不開其他模組,互相依賴耦合嚴重。這種方式是分散式的處理方式,每個元件都是處理和自己相關的業務。管理起來很混亂,如下圖所示:

(借用霜神的幾張圖)

元件化方案調研

那麼按照人腦的思維方式,改成如下這種中心化的方式更加清晰明瞭:

元件化方案調研

但是上面這個圖雖然看起來比剛開始好了許多,但是每個元件還是和mediator雙向依賴,如果改成如下圖所示就完美了:

元件化方案調研

這個時候看起來就舒服多了,每個元件只需要自己管好自己就完了,然後由mediator負責在各個元件中間進行轉發或者跳轉,perfect~~ 那麼如何實現這個架構呢?只要解決下面兩個問題就好了:

  1. mediator作為中介軟體,需要通過某種方式找到每個元件,並能呼叫元件的方法
  2. 每個元件如何得知其他元件提供了哪些方法?只有這樣才可以呼叫對方嘛

原始工程

假設我們現有工程裡面有兩個元件A、B,功能很簡單,如下所示。

#import <UIKit/UIKit.h>

@interface A_VC : UIViewController
-(void)action_A:(NSString*)para1;
@end

==================================

#import "A_VC.h"

@implementation A_VC

-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A %@",para1);
}

@end

複製程式碼
#import <UIKit/UIKit.h>

@interface B_VC : UIViewController

-(void)action_B:(NSString*)para1 para2:(NSInteger)para2;

@end

====================

#import "B_VC.h"

@implementation B_VC

-(void)action_B:(NSString*)para1 para2:(NSInteger)para2{
    NSLog(@"call action_B %@---%zd",para1,para2);
}

@end


複製程式碼

如果是傳統做法,A、B要呼叫對方的功能,就會直接import對方,然後初始化,接著呼叫方法。現在我們對他們實行元件化,改成如上圖所示的mediator方式

target-action方案

該方案藉助OC的runtime特性,實現了服務的自動發現,無需註冊即可實現元件間呼叫。不管是從維護性、可讀性、擴充套件性方面來講,都優於url-scheme方案,也是我比較推崇的元件化方案,下面我們就來看看該方案如何解決上述兩個問題的

Demo演示

此時A、B兩個元件不用改,我們需要加一個mediator,程式碼如下所示:

#import <Foundation/Foundation.h>

@interface Mediator : NSObject

-(void)A_VC_Action:(NSString*)para1;
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2;
+ (instancetype)sharedInstance;

@end

===========================================

#import "Mediator.h"

@implementation Mediator

+ (instancetype)sharedInstance
{
    static Mediator *mediator;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mediator = [[Mediator alloc] init];
    });
    return mediator;
}


-(void)A_VC_Action:(NSString*)para1{
    Class cls = NSClassFromString(@"A_VC");
    NSObject *target = [[cls alloc]init];
    [target performSelector:NSSelectorFromString(@"action_A:") withObject:para1];
}


-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2{
    Class cls = NSClassFromString(@"B_VC");
    NSObject *target = [[cls alloc]init];
    [target performSelector:NSSelectorFromString(@"action_B:para2:") withObject:para1 withObject:para2];
}

@end


複製程式碼

元件B呼叫元件A,如下所示:

    [[Mediator sharedInstance]A_VC_Action:@"引數1"];

複製程式碼

元件A呼叫元件B,如下所示:

    [[Mediator sharedInstance]B_VC_Action:@"引數1" para2:123];

複製程式碼

此時已經可以做到最後一張圖所示的效果了,元件A,B依賴mediator,mediator不依賴元件A,B(也不是完全不依賴,而是把用runtime特性把類的引用弱化為了字串)

反思

看到這裡,大概有人會問,既然用runtime就可以解耦取消依賴,那還要Mediator做什麼?我直接在每個元件裡面用runtime呼叫其他元件不就完了嗎,幹嘛還要多一個mediator?

但是這樣做會存在如下問題:

  1. 呼叫者寫起來很噁心,程式碼提示都沒有, 引數傳遞非常噁心,每次呼叫者都要檢視文件搞清楚每個引數的key是什麼,然後自己去組裝成一個 NSDictionary。維護這個文件和每次都要組裝引數字典很麻煩。
  2. 當呼叫的元件不存在的時候,沒法進行統一處理

那麼加一個mediator的話,就可以做到:

  1. 呼叫者寫起來不噁心,程式碼提示也有了, 引數型別明確。
  2. Mediator可以做統一處理,呼叫某個元件方法時如果某個元件不存在,可以做相應操作,讓呼叫者與元件間沒有耦合。

改進

聰明的讀者可能已經發現上面的mediator方案還是存在一個小瑕疵,受限於performselector方法,最多隻能傳遞兩個引數,如果我想傳遞多個引數怎麼辦呢?

答案是使用字典進行傳遞,此時我們還需要個元件增加一層wrapper,把對外提供的業務全部包裝一次,並且介面的引數全部改成字典。 假設我們現在的B元件需要接受多個引數,如下所示:

-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    NSLog(@"call action_B %@---%zd---%zd----%zd",para1,para2,para3,para4);
}

複製程式碼

那麼此時需要對B元件增加一層wrapper,如下:

#import <Foundation/Foundation.h>

@interface target_B : NSObject
-(void)B_Action:(NSDictionary*)para;

@end

=================
#import "target_B.h"
#import "B_VC.h"

@implementation target_B

-(void)B_Action:(NSDictionary*)para{
    NSString *para1 = para[@"para1"];
    NSInteger para2 = [para[@"para2"]integerValue];
    NSInteger para3 = [para[@"para3"]integerValue];
    NSInteger para4 = [para[@"para4"]integerValue];
    B_VC *VC = [B_VC new];
    [VC action_B:para1 para2:para2 para3:para3 para4:para4];
}
@end

複製程式碼

此時mediator也需要做相應的更改,由原來直接呼叫元件B,改成了呼叫B的wrapper層:

-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    Class cls = NSClassFromString(@"target_B");
    NSObject *target = [[cls alloc]init];
    [target performSelector:NSSelectorFromString(@"B_Action:") withObject:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} ];
}

複製程式碼

現在的元件A呼叫元件B的流程如下所示:

元件化方案調研

此時的專案結構如下:

元件化方案調研

繼續改進

做到這裡,看似比較接近我的要求了,但是還有有點小瑕疵:

  1. Mediator 每一個方法裡都要寫 runtime 方法,格式是確定的,這是可以抽取出來的。
  2. 每個元件對外方法都要在 Mediator 寫一遍,元件一多 Mediator 類的長度是恐怖的。

接著優化就是casa的方案了,我們來看看如何改進,直接看程式碼:

針對第一點,我們可以抽出公共程式碼,當做mediator:

#import "CTMediator.h"
#import <objc/runtime.h>

@interface CTMediator ()

@property (nonatomic, strong) NSMutableDictionary *cachedTarget;

@end

@implementation CTMediator

#pragma mark - public methods
+ (instancetype)sharedInstance
{
    static CTMediator *mediator;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        mediator = [[CTMediator alloc] init];
    });
    return mediator;
}

/*
 scheme://[target]/[action]?[params]
 
 url sample:
 aaa://targetA/actionB?id=1234
 */

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    NSString *urlString = [url query];
    for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
        NSArray *elts = [param componentsSeparatedByString:@"="];
        if([elts count] < 2) continue;
        [params setObject:[elts lastObject] forKey:[elts firstObject]];
    }
    
    // 這裡這麼寫主要是出於安全考慮,防止黑客通過遠端方式呼叫本地模組。這裡的做法足以應對絕大多數場景,如果要求更加嚴苛,也可以做更加複雜的安全邏輯。
    NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
    if ([actionName hasPrefix:@"native"]) {
        return @(NO);
    }
    
    // 這個demo針對URL的路由處理非常簡單,就只是取對應的target名字和method名字,但這已經足以應對絕大部份需求。如果需要擴充,可以在這個方法呼叫之前加入完整的路由邏輯
    id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
    if (completion) {
        if (result) {
            completion(@{@"result":result});
        } else {
            completion(nil);
        }
    }
    return result;
}

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    
    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    Class targetClass;
    
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }
    
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 這裡是處理無響應請求的地方之一,這個demo做得比較簡單,如果沒有可以響應的target,就直接return了。實際開發過程中是可以事先給一個固定的target專門用於在這個時候頂上,然後處理這種請求的
        return nil;
    }
    
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 有可能target是Swift物件
        actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
        action = NSSelectorFromString(actionString);
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 這裡是處理無響應請求的地方,如果無響應,則嘗試呼叫對應target的notFound方法統一處理
            SEL action = NSSelectorFromString(@"notFound:");
            if ([target respondsToSelector:action]) {
                return [self safePerformAction:action target:target params:params];
            } else {
                // 這裡也是處理無響應請求的地方,在notFound都沒有的時候,這個demo是直接return了。實際開發過程中,可以用前面提到的固定的target頂上的。
                [self.cachedTarget removeObjectForKey:targetClassString];
                return nil;
            }
        }
    }
}

- (void)releaseCachedTargetWithTargetName:(NSString *)targetName
{
    NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    [self.cachedTarget removeObjectForKey:targetClassString];
}

#pragma mark - private methods
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    const char* retType = [methodSig methodReturnType];

    if (strcmp(retType, @encode(void)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        return nil;
    }

    if (strcmp(retType, @encode(NSInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(BOOL)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        BOOL result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(CGFloat)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        CGFloat result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(NSUInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSUInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}

#pragma mark - getters and setters
- (NSMutableDictionary *)cachedTarget
{
    if (_cachedTarget == nil) {
        _cachedTarget = [[NSMutableDictionary alloc] init];
    }
    return _cachedTarget;
}

@end

複製程式碼

針對第二點,我們通過把每個元件的對外介面進行分離,剝離到多個mediator的category裡面,感官上把本來在一個mediator裡面實現的對外介面分離到多個category裡面,方便管理

下面展示的是個元件B新增的category,元件A類似

#import "CTMediator.h"

@interface CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;

@end

====================
#import "CTMediator+B_VC_Action.h"

@implementation CTMediator (B_VC_Action)
-(void)B_VC_Action:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    [self performTarget:@"target_B" action:@"B_Action" params:@{@"para1":para1, @"para2":@(para2),@"para3":@(para3),@"para4":@(para4)} shouldCacheTarget:YES];
}
@end

複製程式碼

此時呼叫者只要引入該category,然後呼叫即可,呼叫邏輯其實和上面沒有拆分出category是一樣的。此時的專案結構如下:

元件化方案調研

URL-Scheme方案

這個方案是流傳最廣的,也是最多人使用的,因為Apple本身也提供了url-scheme功能,同時web端也是通過URL的方式進行路由跳轉,那麼很自然的iOS端就借鑑了該方案。

如何實現

Router實現程式碼

#import <Foundation/Foundation.h>
typedef void (^componentBlock) (NSDictionary *param);

@interface URL_Roueter : NSObject
+ (instancetype)sharedInstance;
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk;
- (void)openURL:(NSString *)url withParam:(id)param;
@end

====================


#import "URL_Roueter.h"

@interface URL_Roueter()
@property (nonatomic, strong) NSMutableDictionary *cache;
@end


@implementation URL_Roueter

+ (instancetype)sharedInstance
{
    static URL_Roueter *router;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        router = [[URL_Roueter alloc] init];
    });
    return router;
}



-(NSMutableDictionary *)cache{
    if (!_cache) {
        _cache = [NSMutableDictionary new];
    }
    return _cache;
}


- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
    [self.cache setObject:blk forKey:urlPattern];
}

- (void)openURL:(NSString *)url withParam:(id)param {
    componentBlock blk = [self.cache objectForKey:url];
    if (blk) blk(param);
}


@end

複製程式碼
元件A
#import "A_VC.h"
#import "URL_Roueter.h"

@implementation A_VC

//把自己對外提供的服務(block)用url標記,註冊到路由管理中心元件
+(void)load{
    [[URL_Roueter sharedInstance]registerURLPattern:@"test://A_Action" toHandler:^(NSDictionary* para) {
        NSString *para1 = para[@"para1"];
        [[self new] action_A:para1];
    }];
}


-(void)viewDidLoad{
    [super viewDidLoad];
    UIButton *btn = [UIButton new];
    [btn setTitle:@"呼叫元件B" forState:UIControlStateNormal];
    btn.frame = CGRectMake(100, 100, 100, 50);
    [btn addTarget:self action:@selector(btn_click) forControlEvents:UIControlEventTouchUpInside];
    [btn setBackgroundColor:[UIColor redColor]];
    
    self.view.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
    
}

//呼叫元件B的功能
-(void)btn_click{
    [[URL_Roueter sharedInstance]openURL:@"test://B_Action" withParam:@{@"para1":@"PARA1", @"para2":@(222),@"para3":@(333),@"para4":@(444)}];
}


-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}

@end

複製程式碼

元件B實現的程式碼類似,就不在貼了。上面都是簡化版的實現,不過核心原理是一樣的。

從上面的程式碼可以看出來,實現原理很簡單:每個元件在自己的load方面裡面,把自己對外提供的服務(回撥block)通過url-scheme標記好,然後註冊到URL-Router裡面。

URL-Router接受各個元件的註冊,用字典儲存了每個元件註冊過來的url和對應的服務,只要其他元件呼叫了openURL方法,就會去這個字典裡面根據url找到對應的block執行(也就是執行其他元件提供的服務)

存在的問題

通過url-scheme的方式去做元件化主要存在如下一些問題:

需要專門的管理後臺維護

要提供一個文件專門記錄每個url和服務的對應表,每次元件改動了都要即使修改,很麻煩。引數的格式不明確,是個靈活的 dictionary,同樣需要維護一份文件去查這些引數。

記憶體問題

每個元件在初始化的時候都需要要路由管理中心去註冊自己提供的服務,記憶體裡需要儲存一份表,元件多了會有記憶體問題。

混淆了本地呼叫和遠端呼叫

url-scheme是Apple拿來做app之間跳轉的,或者通過url方式開啟APP,但是上述的方案去把他拿來做本地元件間的跳轉,這會產生問題,大概分為兩點:

  1. 遠端呼叫和本地呼叫的處理邏輯是不同的,正確的做法應該是把遠端呼叫通過一箇中間層轉化為本地呼叫,如果把兩者兩者混為一談,後期可能會出現無法區分業務的情況。比如對於元件無法響應的問題,遠端呼叫可能直接顯示一個404頁面,但是本地呼叫可能需要做其他處理。如果不加以區分,那麼久無法完成這種業務要求。

  2. 遠端呼叫只能傳能被序列化為json的資料,像 UIImage這樣非常規的物件是不行的。所以如果元件介面要考慮遠端呼叫,這裡的引數就不能是這類非常規物件,介面的定義就受限了。出現這種情況的原因就是,遠端呼叫是本地呼叫的子集,這裡混在一起導致元件只能提供子集功能(遠端呼叫),所以這個方案是天生有缺陷的

  3. 理論上來講,元件化是介面層面的東西,應該用語言自身的特性去解決,而url是用於遠端通訊的,不應該和元件化扯上關係

改進

針對上述第二點描述的無法傳遞常規物件的問題,蘑菇街做了改進,通過protocol轉class的方式去實現,但是我想說這種實現辦法真是越高越複雜了。具體看程式碼就知道了

protocolMediator實現:
功能:通過protocol的字串儲存class

#import <Foundation/Foundation.h>

@interface ProtocolMediator : NSObject
+ (instancetype)sharedInstance;
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls;
- (Class)classForProtocol:(Protocol *)proto;

@end

============

#import "ProtocolMediator.h"

@interface ProtocolMediator()
@property (nonatomic,strong) NSMutableDictionary *protocolCache;

@end
@implementation ProtocolMediator


+ (instancetype)sharedInstance
{
static ProtocolMediator *mediator;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    mediator = [[ProtocolMediator alloc] init];
});
return mediator;
}

-(NSMutableDictionary *)protocolCache{
    if (!_protocolCache) {
        _protocolCache = [NSMutableDictionary new];
    }
    return _protocolCache;
}

- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
    [self.protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}

- (Class)classForProtocol:(Protocol *)proto {
    return self.protocolCache[NSStringFromProtocol(proto)];
}


@end

複製程式碼
commonProtocol實現:
功能:所有需要傳遞非常規引數的方法都放在這裡定義,然後各個元件自己去具體實現(這裡為了演示方便,使用的常規的字串和int型別。當然也可以傳遞UIImage等非常規物件)

#import <Foundation/Foundation.h>

@protocol A_VC_Protocol <NSObject>
-(void)action_A:(NSString*)para1;

@end

@protocol B_VC_Protocol <NSObject>
-(void)action_B:(NSString*)para para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4;
@end

複製程式碼
元件A實現:
#import <UIKit/UIKit.h>
#import "CommonProtocol.h"

@interface A_VC : UIViewController<A_VC_Protocol>
@end


=============================

#import "A_VC.h"
#import "ProtocolMediator.h"


@implementation A_VC

//註冊自己的class
+(void)load{
    [[ProtocolMediator sharedInstance] registerProtocol:@protocol(A_VC_Protocol) forClass:[self class]];

}
     

//呼叫元件B,先通過protocol字串取出類class,然後再例項化之呼叫元件B的方法    
-(void)btn_click{
    Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];
    UIViewController<B_VC_Protocol> *B_VC = [[cls alloc] init];
    [B_VC action_B:@"param1" para2:222 para3:333 para4:444];
}


-(void)action_A:(NSString*)para1 {
    NSLog(@"call action_A: %@",para1);
}

@end

複製程式碼
元件B實現
#import <UIKit/UIKit.h>
#import "CommonProtocol.h"


@interface B_VC : UIViewController<B_VC_Protocol>
@end

=============

#import "B_VC.h"
#import "ProtocolMediator.h"

@implementation B_VC

+(void)load{
    [[ProtocolMediator sharedInstance] registerProtocol:@protocol(B_VC_Protocol) forClass:[self class]];
}


-(void)btn_click{
    Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(A_VC_Protocol)];
    UIViewController<A_VC_Protocol> *A_VC = [[cls alloc] init];
    [A_VC action_A:@"param1"];
}


-(void)action_B:(NSString*)para1 para2:(NSInteger)para2 para3:(NSInteger)para3 para4:(NSInteger)para4{
    NSLog(@"call action_B: %@---%zd---%zd---%zd",para1,para2,para3,para4);
}

@end


複製程式碼

原理和缺點

每個元件先通過 Mediator 拿到其他的元件物件class,然後在例項化該class為例項物件,再通過該物件去呼叫它自身實現的protocol方法,因為是通過介面的形式實現的方法,所以任何型別引數都是可以傳遞的。

但是這會導致一個問題:元件方法的呼叫是分散在各地的,沒有統一的入口,也就沒法做元件不存在時的統一處理。

從上面的實現就可以看出來A呼叫B不是直接通過mediator去呼叫,而是先通過mediator生成其他元件的物件,然後自己再用該物件去呼叫其他元件的方法,這就導致元件方法呼叫分散在各個呼叫元件內部,而不能像target-action方案那樣對所有元件的方法呼叫進行統一的管理。

再者這種方式讓元件同時依賴兩個中心:ProtocolMediator和CommonProtocol,依賴越多,後期擴充套件和遷移也會相對困難。

並且這種呼叫其他元件的方式有點詭異,不是正常的使用方法,一般都是直接你發起一個呼叫請求,其他元件直接把執行結果告訴你,但是這裡確實給你返回一個元件物件,讓你自己在用這個物件去發起請求,這操作有點蛋疼。。。

總結

其實蘑菇街的url-scheme加上protocol-class方案一起提供元件間跳轉和呼叫會讓人無所適從,使用者還要區分不同的引數要使用的不同的方法,而target-action方案可以用相同的方法來傳遞任意引數。綜上所述,target-action方案更優。

Demo下載

  1. url-scheme
  2. protocol-class
  3. target-action

元件化方案實施

從早上起床寫到凌晨,實在寫不動了,留個坑,過年來在寫。

收拾收拾行李準備回家過年啦,提前給大家拜個早年 ~~~

相關文章