iOS 組件化路由

路由的意義

路由并非只是指的界面跳轉(zhuǎn),還包括數(shù)據(jù)獲取等幾乎所有業(yè)務(wù)。

(一)項(xiàng)目中WKWebview& UIWebView 與原生Native 交互路由

路由URL 例如 QJ://detail?name=xx&id=xxx

缺點(diǎn)很明顯:字符串 URI 并不能表征 iOS 系統(tǒng)原生類型,要閱讀對(duì)應(yīng)模塊的使用文檔,大量的硬編碼
項(xiàng)目中實(shí)現(xiàn)代碼大概就是這樣

//先維護(hù)一張路由表 
- (NSDictionary *)getHostDictionary {

    static NSDictionary *dicts = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
       dicts = @{
        HOST_LOGOUT:  @"logoutAction:",
        HOST_ORDER_DETAIL:  @"orderDetail:"
      };
    });
   return dicts;
}

- (UIViewController *)logoutAction:(NSDictionary *)param {
//做一些登錄登出的業(yè)務(wù)邏輯
}

通俗來(lái)講
解析URI ---> 等到 target params ---> 調(diào)用原生

三、組件化的意義

前面對(duì)路由的分析提到了使用目標(biāo)和參數(shù) (aim/params) 動(dòng)態(tài)定位到具體業(yè)務(wù)的技術(shù)點(diǎn)。實(shí)際上在 iOS Objective-C 中大概有反射依賴注入兩種思路:

  • aim轉(zhuǎn)化為具體的ClassSEL,利用 runtime 運(yùn)行時(shí)調(diào)用到具體業(yè)務(wù)。
  • 對(duì)于代碼來(lái)說(shuō),進(jìn)程空間是共享的,所以維護(hù)一個(gè)全局的映射表,提前將aim映射到一段代碼,調(diào)用時(shí)執(zhí)行具體業(yè)務(wù)。

可以明確的是,這兩種方式都已經(jīng)讓Mediator免去了對(duì)業(yè)務(wù)模塊的依賴:

16c40c9fb110ace5.png

而這些解耦技術(shù),正是 iOS 組件化的核心。

組件化主要目的是為了讓各個(gè)業(yè)務(wù)模塊獨(dú)立運(yùn)行,互不干擾,那么業(yè)務(wù)模塊之間的完全解耦是必然的,同時(shí)對(duì)于業(yè)務(wù)模塊的拆分也非??季?,更應(yīng)該追求功能獨(dú)立而不是最小粒度。

(一) Runtime 解耦

為 Router 定義了一個(gè)統(tǒng)一入口方法:

/// 此方法就是一個(gè)攔截器,可做容錯(cuò)以及動(dòng)態(tài)調(diào)度
- (id)performTarget:(NSString *)target action:(NSString *)action params:(NSDictionary *)params {
    Class cls; id obj; SEL sel;
    cls = NSClassFromString(target);
    if (!cls) goto fail;
    sel = NSSelectorFromString(action);
    if (!sel) goto fail;
    obj = [cls new];
    if (![obj respondsToSelector:sel]) goto fail;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [obj performSelector:sel withObject:params];
#pragma clang diagnostic pop
fail:
    NSLog(@"找不到目標(biāo),寫(xiě)容錯(cuò)邏輯");
    return nil;
}
復(fù)制代碼

簡(jiǎn)單寫(xiě)了下代碼,原理很簡(jiǎn)單,可用 Demo 測(cè)試。對(duì)于內(nèi)部調(diào)用,為每一個(gè)模塊寫(xiě)一個(gè)分類:

@implementation BMediator (BAim)
- (void)gotoBAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
    [self performTarget:@"BTarget" action:@"gotoBAimController:" params:@{@"name":name, @"callBack":callBack}];
}
@end
復(fù)制代碼

可以看到這里是給BTarget發(fā)送消息:

@interface BTarget : NSObject
- (void)gotoBAimController:(NSDictionary *)params; 
@end
@implementation BTarget
- (void)gotoBAimController:(NSDictionary *)params {
    BAimController *vc = [BAimController new];
    vc.name = params[@"name"];
    vc.callBack = params[@"callBack"];
    [UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
}
@end
復(fù)制代碼

為什么要定義分類

定義分類的目的前面也說(shuō)了,相當(dāng)于一個(gè)語(yǔ)法糖,讓調(diào)用者輕松使用,讓 hard code 交給對(duì)應(yīng)的業(yè)務(wù)工程師。

為什么要定義 Target “靶子”

  • 避免同一模塊路由邏輯散落各地,便于管理。
  • 路由并非只有控制器跳轉(zhuǎn),某些業(yè)務(wù)可能無(wú)法放代碼(比如網(wǎng)絡(luò)請(qǐng)求就需要額外創(chuàng)建類來(lái)接受路由調(diào)用)。
  • 便于方案的接入和摒棄(靈活性)。

可能有些人對(duì)這些類的管理存在疑慮,下圖就表示它們的關(guān)系(一個(gè)塊表示一個(gè) repo):

image.png

圖中“注意”處箭頭,B 模塊是否需要引入它自己的分類 repo,取決于是否需要做所有界面跳轉(zhuǎn)的攔截,如果需要那么 B 模塊仍然要引入自己的 repo 使用。

完整的方案和代碼可以查看 Casa 的 CTMediator,設(shè)計(jì)得比較完備,筆者沒(méi)挑出什么毛病。

(二) Block 解耦

下面簡(jiǎn)單實(shí)現(xiàn)了兩個(gè)方法:

- (void)registerKey:(NSString *)key block:(nonnull id _Nullable (^)(NSDictionary * _Nullable))block {
    if (!key || !block) return;
    self.map[key] = block;
}
/// 此方法就是一個(gè)攔截器,可做容錯(cuò)以及動(dòng)態(tài)調(diào)度
- (id)excuteBlockWithKey:(NSString *)key params:(NSDictionary *)params {
    if (!key) return nil;
    id(^block)(NSDictionary *) = self.map[key];
    if (!block) return nil;
    return block(params);
}
復(fù)制代碼

維護(hù)一個(gè)全局的字典 (Key -> Block),只需要保證閉包的注冊(cè)在業(yè)務(wù)代碼跑起來(lái)之前,很容易想到在+load中寫(xiě):

@implementation DRegister
+ (void)load {
    [DMediator.share registerKey:@"gotoDAimKey" block:^id _Nullable(NSDictionary * _Nullable params) {
        DAimController *vc = [DAimController new];
        vc.name = params[@"name"];
        vc.callBack = params[@"callBack"];
        [UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
        return nil;
    }];
}
@end
復(fù)制代碼

至于為什么要使用一個(gè)單獨(dú)的DRegister類,和前面“Runtime 解耦”為什么要定義一個(gè)Target是一個(gè)道理。同樣的,使用一個(gè)分類來(lái)簡(jiǎn)化內(nèi)部調(diào)用(這是蘑菇街方案可以優(yōu)化的地方):

@implementation DMediator (DAim)
- (void)gotoDAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
    [self excuteBlockWithKey:@"gotoDAimKey" params:@{@"name":name, @"callBack":callBack}];
}
@end
復(fù)制代碼

可以看到,Block 方案和 Runtime 方案 repo 架構(gòu)上可以基本一致(見(jiàn)圖6),只是 Block 多了注冊(cè)這一步。

為了靈活性,Demo 中讓 Key -> Block,這就讓 Block 里面要寫(xiě)很多代碼,如果縮小范圍將 Key -> UIViewController.class 可以減少注冊(cè)的代碼量,但這樣又難以覆蓋所有場(chǎng)景。

注冊(cè)所產(chǎn)生的內(nèi)存占用并不是負(fù)擔(dān),主要是大量的注冊(cè)可能會(huì)明顯拖慢啟動(dòng)速度。

(三) Protocol 解耦

這種方式仍然要注冊(cè),使用一個(gè)全局的字典 (Protocol -> Class) 存儲(chǔ)起來(lái)。

- (void)registerService:(Protocol *)service class:(Class)cls {
    if (!service || !cls) return;
    self.map[NSStringFromProtocol(service)] = cls;
}
- (id)getObject:(Protocol *)service {
    if (!service) return nil;
    Class cls = self.map[NSStringFromProtocol(service)];
    id obj = [cls new];
    if ([obj conformsToProtocol:service]) {
        return obj;
    }
    return nil;
}
復(fù)制代碼

定義一個(gè)協(xié)議服務(wù):

@protocol CAimService <NSObject>
- (void)gotoCAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack;
@end
復(fù)制代碼

用一個(gè)類實(shí)現(xiàn)協(xié)議并且注冊(cè)協(xié)議:

@implementation CAimServiceProvider
+ (void)load {
    [CMediator.share registerService:@protocol(CAimService) class:self];
}
#pragma mark - <CAimService>
- (void)gotoCAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
    CAimController *vc = [CAimController new];
    vc.name = name;
    vc.callBack = callBack;
    [UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
}
@end
復(fù)制代碼

至于為什么要使用一個(gè)單獨(dú)的ServiceProvider類,和前面“Runtime 解耦”為什么要定義一個(gè)Target是一個(gè)道理。

使用起來(lái)很優(yōu)雅:

id<CAimService> service = [CMediator.share getObject:@protocol(CAimService)];
[service gotoCAimControllerWithName:@"From C" callBack:^{
       NSLog(@"CAim CallBack");
}];
復(fù)制代碼

看起來(lái)這種方案不需要硬編碼很舒服,但是它有個(gè)致命的問(wèn)題 ——— 無(wú)法攔截所有路由方法。

這也就意味著這種方案做不了自動(dòng)化動(dòng)態(tài)調(diào)用。

阿里的 BeeHive 是目前的最佳實(shí)踐。注冊(cè)部分它可以將待注冊(cè)的類字符串寫(xiě)入 Data 段,然后在 Image 加載的時(shí)候讀取出來(lái)注冊(cè)。這個(gè)操作只是將注冊(cè)的執(zhí)行放到了+load方法之前,仍然會(huì)拖慢啟動(dòng)速度,所以這個(gè)處理并不是為了提速,而是將注冊(cè)代碼更加優(yōu)雅的分散到具體業(yè)務(wù)方。

為什么 Protocol -> Class 和 Key -> Block 需要注冊(cè)?

想象一下,解耦意味著調(diào)用方只有系統(tǒng)原生的標(biāo)識(shí),如何定位到目標(biāo)業(yè)務(wù)? 必然有個(gè)映射。 而 runtime 可以直接調(diào)用目標(biāo)業(yè)務(wù),其它兩種方式只有建立映射表。 當(dāng)然 Protocol 方式也可以不建立映射表,直接遍歷所有類,找出遵循這個(gè)協(xié)議的類也能找到,不過(guò)明顯這樣是低效且不安全的。

組件化總結(jié)

經(jīng)過(guò)對(duì)比作者選擇的方式是 target-action + runtime
本文Demo

參考文章
解讀 iOS 組件化與路由的本質(zhì)

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容