iOS--谈一谈模块化架构(附Demo)

作者: kirito_song | 来源:发表于2018-10-25 17:33 被阅读78次

    目录

    • 先说说模块化
    • 如何将中间层与业务层剥离
    • performSelector与协议的异同
    • 调用方式
    • 中间件的路由策略
    • 模块入口
    • 低版本兼容
    • 重定向路由
    • 项目的结构
    • 模块化的程度
    • 哪些模块适合下沉
    • 关于协作开发
    • 效果演示

    先说说模块化

    网上有很多谈模块化的文章、这里有一篇《IOS-组件化架构漫谈》有兴趣可以读读。

    总之有三个阶段

    MVC模式下、我们的总工程长这样:
    加一个中间层、负责调用指定文件
    将中间层与模块进行解耦

    如何将中间层与业务层剥离

    • 刚才第二张图里的基本原理:

    将原本在业务文件(KTHomeViewController)代码里的耦合代码

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

    转移到中间层(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];
    

    看似业务之间相互解耦、但是中间层将要引用所有的业务模块。
    直接把耦合的对象转移了而已。

    • 解耦的方式

    想要解耦、前提就是不引用头文件。
    那么、通过字符串代替头文件的引用就是了。
    简单来讲有两种方式:

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

    具体使用上

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

    但这样有一个问题、就是返回值如果不为id类型、有几率造成崩溃。
    不过这可以通过NSInvocation进行弥补。
    这段代码摘自《iOS从零到一搭建组件化项目架构》

    - (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. 利用协议的方式调用未知对象方法(这也是我使用的方式)

    首先你需要一个协议:

    @protocol KTComponentManagerProtocol <NSObject>
    
    + (id)handleAction:(NSString *)action params:(NSDictionary *)params;
    
    @end
    

    然后调用:

    if ([targetClass respondsToSelector:@selector(handleAction:params:)]) {
         //向已经注册的对象发送Action信息
         returnObj = [targetClass handleAction:actionName params:params];
    }else {
         //未注册的、进行进一步处理。比如上报啊、返回一个占位对象啊等等
         NSLog(@"未注册的方法");
    }
    

    如果有返回基本类型可以在具体入口文件里处理:

    + (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与协议的异同

    以上两种方式的中心思想基本相同、也有许多共同点:
    1. 需要用字典方式传递参数
    2. 需要处理返回值为非id的情况
      只不过一个交给路由、一个交给具体模块。
    协议相比performSelector当然也有不同:
    1. 突破了performSelector最多只能传递一个参数的限制、并且你可以定制自己想要的格式
    + (id)handleAction:(NSString *)action params:(NSDictionary *)params;
    
    1. 具体方法的调用、协议要多一层调用
      handleAction方法根据具体的action代替performSelector进行动作的分发。

    不过我还是觉得第二种方便、因为你的performSelector与实际调用的方法、也解耦了。
    比如有一天你换了方法:
    performSelector的方式还需要修改整个url、以保证调用到正确的Selector
    而协议则不然、你可以在handleAction方法的内部进行二次路由。


    调用方式

    • 中间件调用模块

    这里我做了两种方案、一种纯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}];
    

    这两种方式都会用到、区别随后再说。

    • 模块间调用

    用上面的方式直接调用也可以、但是容易写错。
    通过为中间件加入Category的方式、对接口进行约束。
    并且将url以及参数的拼装工作交给对应模块的开发人员。

    @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];
    

    中间件的路由策略

    • 远程路由 && 降级路由
    - (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)];
        
        //可以对url进行路由。比如从服务器下发json文件。将AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE这样
        if (self.redirectionjson[path]) {
            path = self.redirectionjson[path];
        }
        
        //如果该target的action已经注册
        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页面、打开H5页面
            //webUrlSet可以由服务器下发
            NSLog(@"跳转网页:%@",url);
            
        }
        
        return returnObj;
    }
    

    远程路由需要考虑由于本地版本过低导致需要跳转H5的情况。
    如果本地支持、则直接使用本地路由。

    • 本地路由
    - (id)openUrl:(NSString *)url params:(NSDictionary *)params {
        id returnObj;
        
        if (url.length == 0) {
            return nil;
        }
        
        //可以对url进行路由。比如从服务器下发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:)]) {
            //向已经实现了协议的对象发送Target&&Action信息
            returnObj = [targetClass handleAction:actionName params:params];
        }else {
            //未注册的、进行进一步处理。比如上报啊、返回一个占位对象啊等等
            NSLog(@"未注册的方法");
        }
    
        return returnObj;
    }
    

    通过调用模块入口模块targetClass遵循的中间件协议方法handleAction:params:将动作action以及参数params传递。


    模块入口

    模块入口实现了中间件的协议方法handleAction:params:
    根据不同的Action、内部自己负责逻辑处理。

    #import "ModuleHandlerForLogin.h"
    #import "KTLoginManager.h"
    #import "KTComponentManager+LoginModule.h"
    
    @implementation ModuleHandlerForLogin
    
    /**
     相当于每个模块维护自己的注册表
     */
    + (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;
    }
    

    低版本兼容

    有时低版本的App也可能被远程进行路由、但却并没有原生页面。

    这时、如果有H5页面、则需要跳转H5

    //如果该target的action已经注册
    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页面、打开H5页面
        //webUrlSet可以由服务器下发
        NSLog(@"跳转网页:%@",url);
    }
    

    registeredDic负责维护注册表、记录了本地模块实现了那些Target && Action。
    这个注册动作、交给每个模块的入口进行:

    /**
     在load中向模块管理器注册
     
     这里其实如果引入KTComponentManager会方便很多
     但是会依赖管理中心、所以算了
     
     */
    + (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
    
    }
    

    重定向路由

    由于某些原因、有时我们需要修改某些Url路由的指向(比如顺风车?)

    //可以对url进行路由。比如从服务器下发json文件。将AAAA/BBBB路由到AAAA/DDDD或者CCCC/EEEE这样
    if (self.redirectionjson[path]) {
        path = self.redirectionjson[path];
    }
    

    这个redirectionjson由服务器下发、本地路由时如果发现有需要被重定向的Path则进行重定向动作、修改路由的目的地。


    项目的结构

    模块全部以私有Pods的形式引入、单个模块内部遵循MVC(随便你用什么MVP啊、MVVM啊。只要别引入其他模块的东西)。

    我只是写一个demo、所以嫌麻烦没有搞Pods。意会吧。


    模块化的程度

    每个模块、引入了公共模块之后。
    可以在自己的Target工程独立运行。


    哪些模块适合下沉

    可以跨产品使用的模块

    日志、网络层、三方SDK、持久化、分享、工具扩展等等。


    关于协作开发

    pods一定要保证版本的清晰、比如Category哪怕只更新了一个入口、也要当做一个新的版本。

    于是开发的阶段由于要经常更新代码、最好还是不要用pods。
    大家可以写好Category在自己模块的Target先工作。

    最后调试上线的时候再统一上传pods并且打包。


    效果演示

    写了三个按钮

    - (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"]];
    }
    
    //这里应该用通知获取的
    - (void)didLoginIn {
        [self.loginBtn setTitle:@"退出登录" forState:UIControlStateNormal];
    }
    
    - (void)didLoginOut {
        [self.loginBtn setTitle:@"登录" forState:UIControlStateNormal];
    }
    
    
    
    

    Demo


    最后

    本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果愿意补充以及不吝赐教小弟会更加感激。

    相关文章

      网友评论

        本文标题:iOS--谈一谈模块化架构(附Demo)

        本文链接:https://www.haomeiwen.com/subject/euzqtqtx.html