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

栏目: IOS · 发布时间: 6年前

内容简介:目录先说说模块化网上有很多谈模块化的文章、这里有一篇

目录

  • 先说说模块化

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

  • performSelector与协议的异同

  • 调用方式

  • 中间件的路由策略

  • 模块入口

  • 低版本兼容

  • 重定向路由

  • 项目的结构

  • 模块化的程度

  • 哪些模块适合下沉

  • 关于协作开发

  • 效果演示

先说说模块化

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

总之有三个阶段

MVC模式下、我们的总工程长这样:

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

加一个中间层、负责调用指定文件

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

将中间层与模块进行解耦

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

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

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

将原本在业务文件(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:¶ms 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:¶ms 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:¶ms 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:¶ms 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:¶ms 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
}

2.利用协议的方式调用未知对象方法(这也是我使用的方式)

首先你需要一个协议:

@protocol KTComponentManagerProtocol + (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;

2.具体方法的调用、协议要多一层调用

由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啊。只要别引入其他模块的东西)。

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

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

模块化的程度

每个模块、引入了公共模块之后。

可以在自己的Target工程独立运行。

哪些模块适合下沉

可以跨产品使用的模块

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

关于协作开发

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

于是开发的阶段由于要经常更新代码、最好还是不要用pods。

大家可以写好Category在自己模块的Target先工作。

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

效果演示

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

写了三个按钮

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

最后

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

作者:kirito_song

链接:https://www.jianshu.com/p/d5630a3c8516


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python for Data Analysis

Python for Data Analysis

Wes McKinney / O'Reilly Media / 2012-11-1 / USD 39.99

Finding great data analysts is difficult. Despite the explosive growth of data in industries ranging from manufacturing and retail to high technology, finance, and healthcare, learning and accessing d......一起来看看 《Python for Data Analysis》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试