iOS--談一談模塊化架構(gòu)(附Demo)


目錄

  • 先說(shuō)說(shuō)模塊化
  • 如何將中間層與業(yè)務(wù)層剝離
  • performSelector與協(xié)議的異同
  • 調(diào)用方式
  • 中間件的路由策略
  • 模塊入口
  • 低版本兼容
  • 重定向路由
  • 項(xiàng)目的結(jié)構(gòu)
  • 模塊化的程度
  • 哪些模塊適合下沉
  • 關(guān)于協(xié)作開(kāi)發(fā)
  • 效果演示

先說(shuō)說(shuō)模塊化

網(wǎng)上有很多談模塊化的文章、這里有一篇《IOS-組件化架構(gòu)漫談》有興趣可以讀讀。

總之有三個(gè)階段

MVC模式下、我們的總工程長(zhǎng)這樣:
加一個(gè)中間層、負(fù)責(zé)調(diào)用指定文件
將中間層與模塊進(jìn)行解耦

如何將中間層與業(yè)務(wù)層剝離

  • 剛才第二張圖里的基本原理:

將原本在業(yè)務(wù)文件(KTHomeViewController)代碼里的耦合代碼

KTAModuleUserViewController * vc = [[KTAModuleUserViewController alloc]initWithUserName:@"kirito" age:18];
[self.navigationController pushViewController:vc animated:YES];

轉(zhuǎn)移到中間層(KTComponentManager)中

//KTHomeViewController.h  

UIViewController * vc = [[KTComponentManager sharedInstance] ModuleA_getUserViewControllerWithUserName:@"kirito" age:18];
[self.navigationController pushViewController:vc animated:YES];

//KTComponentManager.h
return [[KTAModuleUserViewController alloc]initWithUserName:userName age:age];

看似業(yè)務(wù)之間相互解耦、但是中間層將要引用所有的業(yè)務(wù)模塊。
直接把耦合的對(duì)象轉(zhuǎn)移了而已。

  • 解耦的方式

想要解耦、前提就是不引用頭文件。
那么、通過(guò)字符串代替頭文件的引用就是了。
簡(jiǎn)單來(lái)講有兩種方式:

1. - (id)performSelector:(SEL)aSelector withObject:(id)object;

具體使用上

Class targetClass = NSClassFromString(@"targetName");
SEL action = NSSelectorFromString(@"ActionName");
return [target performSelector:action withObject:params];

但這樣有一個(gè)問(wèn)題、就是返回值如果不為id類型、有幾率造成崩潰。
不過(guò)這可以通過(guò)NSInvocation進(jìn)行彌補(bǔ)。
這段代碼摘自《iOS從零到一搭建組件化項(xiàng)目架構(gòu)》

- (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
}
  1. 利用協(xié)議的方式調(diào)用未知對(duì)象方法(這也是我使用的方式)

首先你需要一個(gè)協(xié)議:

@protocol KTComponentManagerProtocol <NSObject>

+ (id)handleAction:(NSString *)action params:(NSDictionary *)params;

@end

然后調(diào)用:

if ([targetClass respondsToSelector:@selector(handleAction:params:)]) {
     //向已經(jīng)注冊(cè)的對(duì)象發(fā)送Action信息
     returnObj = [targetClass handleAction:actionName params:params];
}else {
     //未注冊(cè)的、進(jìn)行進(jìn)一步處理。比如上報(bào)啊、返回一個(gè)占位對(duì)象啊等等
     NSLog(@"未注冊(cè)的方法");
}

如果有返回基本類型可以在具體入口文件里處理:

+ (id)handleAction:(NSString *)action params:(NSDictionary *)params {
    id returnValue = nil;

    if ([action isEqualToString:@"isLogin"]) {
        returnValue = @([[KTLoginManager sharedInstance] isLogin]);
    }
    if ([action isEqualToString:@"loginIfNeed"]) {
        returnValue = @([[KTLoginManager sharedInstance] loginIfNeed]);
    }
    
    if ([action isEqualToString:@"loginOut"]) {
        [[KTLoginManager sharedInstance] loginOut];
    }
    return returnValue;
}

performSelector與協(xié)議的異同

以上兩種方式的中心思想基本相同、也有許多共同點(diǎn):
  1. 需要用字典方式傳遞參數(shù)
  2. 需要處理返回值為非id的情況
    只不過(guò)一個(gè)交給路由、一個(gè)交給具體模塊。
協(xié)議相比performSelector當(dāng)然也有不同:
  1. 突破了performSelector最多只能傳遞一個(gè)參數(shù)的限制、并且你可以定制自己想要的格式
+ (id)handleAction:(NSString *)action params:(NSDictionary *)params;
  1. 具體方法的調(diào)用、協(xié)議要多一層調(diào)用
    handleAction方法根據(jù)具體的action代替performSelector進(jìn)行動(dòng)作的分發(fā)。

不過(guò)我還是覺(jué)得第二種方便、因?yàn)槟愕?code>performSelector與實(shí)際調(diào)用的方法、也解耦了。
比如有一天你換了方法:
performSelector的方式還需要修改整個(gè)url、以保證調(diào)用到正確的Selector。
而協(xié)議則不然、你可以在handleAction方法的內(nèi)部進(jìn)行二次路由。


調(diào)用方式

  • 中間件調(diào)用模塊

這里我做了兩種方案、一種純Url一種帶參

UIViewController *vc = [self openUrl:[NSString stringWithFormat:@"https://www.bilibili.com/KTModuleHandlerForA/getUserViewController?userName=%@&age=%d",userName,age]];

NSNumber *value = [self openUrl:@"ModuleHandlerForLogin/loginIfNeed" params:@{@"delegate":delegate}];

這兩種方式都會(huì)用到、區(qū)別隨后再說(shuō)。

  • 模塊間調(diào)用

用上面的方式直接調(diào)用也可以、但是容易寫(xiě)錯(cuò)。
通過(guò)為中間件加入Category的方式、對(duì)接口進(jìn)行約束。
并且將url以及參數(shù)的拼裝工作交給對(duì)應(yīng)模塊的開(kāi)發(fā)人員。

@interface KTComponentManager (ModuleA)

- (UIViewController *)ModuleA_getUserViewControllerWithUserName:(NSString *)userName age:(int)age;

@end

然后直接代用中間件的Category接口

UIViewController * vc = [[KTComponentManager sharedInstance] ModuleA_getUserViewControllerWithUserName:@"kirito" age:18];
    [self.navigationController pushViewController:vc animated:YES];

中間件的路由策略

  • 遠(yuǎn)程路由 && 降級(jí)路由
- (id)openUrl:(NSString *)url{
    id returnObj;
    
    NSURL * openUrl = [NSURL URLWithString:url];
    NSString * path = [openUrl.path substringWithRange:NSMakeRange(1, openUrl.path.length - 1)];
    
    NSRange range = [path rangeOfString:@"/"];
    NSString *targetName = [path substringWithRange:NSMakeRange(0, range.location)];
    NSString *actionName = [path substringWithRange:NSMakeRange(range.location + 1, path.length - range.location - 1)];
    
    //可以對(duì)url進(jìn)行路由。比如從服務(wù)器下發(fā)json文件。將AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE這樣
    if (self.redirectionjson[path]) {
        path = self.redirectionjson[path];
    }
    
    //如果該target的action已經(jīng)注冊(cè)
    if ([self.registeredDic[targetName] containsObject:actionName]) {
        returnObj = [self openUrl:path params:[self getURLParameters:openUrl.absoluteString]];
    }else if ([self.webUrlSet containsObject:[NSString stringWithFormat:@"%@%@",openUrl.host,openUrl.path]]){
        //低版本兼容
        //如果有某些H5頁(yè)面、打開(kāi)H5頁(yè)面
        //webUrlSet可以由服務(wù)器下發(fā)
        NSLog(@"跳轉(zhuǎn)網(wǎng)頁(yè):%@",url);
        
    }
    
    return returnObj;
}

遠(yuǎn)程路由需要考慮由于本地版本過(guò)低導(dǎo)致需要跳轉(zhuǎn)H5的情況。
如果本地支持、則直接使用本地路由。

  • 本地路由
- (id)openUrl:(NSString *)url params:(NSDictionary *)params {
    id returnObj;
    
    if (url.length == 0) {
        return nil;
    }
    
    //可以對(duì)url進(jìn)行路由。比如從服務(wù)器下發(fā)json文件。將AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE這樣
    if (self.redirectionjson[url]) {
        url = self.redirectionjson[url];
    }
    
    
    NSRange range = [url rangeOfString:@"/"];
    
    NSString *targetName = [url substringWithRange:NSMakeRange(0, range.location)];
    NSString *actionName = [url substringWithRange:NSMakeRange(range.location + 1, url.length - range.location - 1)];
    

    Class targetClass = NSClassFromString(targetName);
    
    
    if ([targetClass respondsToSelector:@selector(handleAction:params:)]) {
        //向已經(jīng)實(shí)現(xiàn)了協(xié)議的對(duì)象發(fā)送Target&&Action信息
        returnObj = [targetClass handleAction:actionName params:params];
    }else {
        //未注冊(cè)的、進(jìn)行進(jìn)一步處理。比如上報(bào)啊、返回一個(gè)占位對(duì)象啊等等
        NSLog(@"未注冊(cè)的方法");
    }

    return returnObj;
}

通過(guò)調(diào)用模塊入口模塊targetClass遵循的中間件協(xié)議方法handleAction:params:將動(dòng)作action以及參數(shù)params傳遞。


模塊入口

模塊入口實(shí)現(xiàn)了中間件的協(xié)議方法handleAction:params:
根據(jù)不同的Action、內(nèi)部自己負(fù)責(zé)邏輯處理。

#import "ModuleHandlerForLogin.h"
#import "KTLoginManager.h"
#import "KTComponentManager+LoginModule.h"

@implementation ModuleHandlerForLogin

/**
 相當(dāng)于每個(gè)模塊維護(hù)自己的注冊(cè)表
 */
+ (id)handleAction:(NSString *)action params:(NSDictionary *)params {
    id returnValue = nil;
    if ([action isEqualToString:@"getUserViewController"]) {
        
        returnValue = [[KTAModuleUserViewController alloc]initWithUserName:params[@"userName"] age:[params[@"age"] intValue]];
    }
    return returnValue;
}

低版本兼容

有時(shí)低版本的App也可能被遠(yuǎn)程進(jìn)行路由、但卻并沒(méi)有原生頁(yè)面。

這時(shí)、如果有H5頁(yè)面、則需要跳轉(zhuǎn)H5

//如果該target的action已經(jīng)注冊(cè)
if ([self.registeredDic[targetName] containsObject:actionName]) {
    returnObj = [self openUrl:path params:[self getURLParameters:openUrl.absoluteString]];
}else if ([self.webUrlSet containsObject:[NSString stringWithFormat:@"%@%@",openUrl.host,openUrl.path]]){
    //低版本兼容
    //如果有某些H5頁(yè)面、打開(kāi)H5頁(yè)面
    //webUrlSet可以由服務(wù)器下發(fā)
    NSLog(@"跳轉(zhuǎn)網(wǎng)頁(yè):%@",url);
}

registeredDic負(fù)責(zé)維護(hù)注冊(cè)表、記錄了本地模塊實(shí)現(xiàn)了那些Target && Action。
這個(gè)注冊(cè)動(dòng)作、交給每個(gè)模塊的入口進(jìn)行:

/**
 在load中向模塊管理器注冊(cè)
 
 這里其實(shí)如果引入KTComponentManager會(huì)方便很多
 但是會(huì)依賴管理中心、所以算了
 
 */
+ (void)load {

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"

    Class KTComponentManagerClass = NSClassFromString(@"KTComponentManager");
    SEL sharedInstance = NSSelectorFromString(@"sharedInstance");
    id KTComponentManager = [KTComponentManagerClass performSelector:sharedInstance];
    SEL addHandleTargetWithInfo = NSSelectorFromString(@"addHandleTargetWithInfo:");
    
    NSMutableSet * actionSet = [[NSMutableSet alloc]initWithArray:@[@"getUserViewController"]];
    
    NSDictionary * targetInfo = @{
                                  @"targetName":@"KTModuleHandlerForA",
                                  @"actionSet":actionSet
                                  };
    
    [KTComponentManager performSelector:addHandleTargetWithInfo withObject:targetInfo];

    #pragma clang diagnostic pop

}

重定向路由

由于某些原因、有時(shí)我們需要修改某些Url路由的指向(比如順風(fēng)車?)

//可以對(duì)url進(jìn)行路由。比如從服務(wù)器下發(fā)json文件。將AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE這樣
if (self.redirectionjson[path]) {
    path = self.redirectionjson[path];
}

這個(gè)redirectionjson由服務(wù)器下發(fā)、本地路由時(shí)如果發(fā)現(xiàn)有需要被重定向的Path則進(jìn)行重定向動(dòng)作、修改路由的目的地。


項(xiàng)目的結(jié)構(gòu)

模塊全部以私有Pods的形式引入、單個(gè)模塊內(nèi)部遵循MVC(隨便你用什么MVP啊、MVVM啊。只要?jiǎng)e引入其他模塊的東西)。

我只是寫(xiě)一個(gè)demo、所以嫌麻煩沒(méi)有搞Pods。意會(huì)吧。


模塊化的程度

每個(gè)模塊、引入了公共模塊之后。
可以在自己的Target工程獨(dú)立運(yùn)行。


哪些模塊適合下沉

可以跨產(chǎn)品使用的模塊

日志、網(wǎng)絡(luò)層、三方SDK、持久化、分享、工具擴(kuò)展等等。


關(guān)于協(xié)作開(kāi)發(fā)

pods一定要保證版本的清晰、比如Category哪怕只更新了一個(gè)入口、也要當(dāng)做一個(gè)新的版本。

于是開(kāi)發(fā)的階段由于要經(jīng)常更新代碼、最好還是不要用pods。
大家可以寫(xiě)好Category在自己模塊的Target先工作。

最后調(diào)試上線的時(shí)候再統(tǒng)一上傳pods并且打包。


效果演示

寫(xiě)了三個(gè)按鈕

- (IBAction)pushToModuleAUserVC:(UIButton *)sender {
    
    if (![[KTComponentManager sharedInstance] loginIfNeedWithDelegate:self]) {
        return;
    }
    
    UIViewController * vc = [[KTComponentManager sharedInstance] ModuleA_getUserViewControllerWithUserName:@"kirito" age:18];
    [self.navigationController pushViewController:vc animated:YES];
    
}
- (IBAction)LoginBtnClick:(UIButton *)sender {
    
    if ([[KTComponentManager sharedInstance] loginIfNeedWithDelegate:self]) {
        [[KTComponentManager sharedInstance] loginOutWithDelegate:self];
    }
    
}

- (IBAction)openWebUrl:(id)sender {
    [[KTComponentManager sharedInstance] openUrl:[NSString stringWithFormat:@"https://www.bilibili.com/video/av25305807"]];
}

//這里應(yīng)該用通知獲取的
- (void)didLoginIn {
    [self.loginBtn setTitle:@"退出登錄" forState:UIControlStateNormal];
}

- (void)didLoginOut {
    [self.loginBtn setTitle:@"登錄" forState:UIControlStateNormal];
}



Demo


最后

本文主要是自己的學(xué)習(xí)與總結(jié)。如果文內(nèi)存在紕漏、萬(wàn)望留言斧正。如果愿意補(bǔ)充以及不吝賜教小弟會(huì)更加感激。

最后編輯于
?著作權(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ù)。

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