iOS 组件化方案总结

iOS 组件化方案总结组件化初探为什么需要组件化模块间解耦模块重用提高团队协作开发效率便于单元测试哪些项目不需要组件化项目较小,模块之间交互简单、耦合少模块没有被多个外部模块引用,只是一个简单的小模块模块不需要重用,代码也

组件化初探

为什么需要组件化

  • 模块间解耦
  • 模块重用
  • 提高团队协作开发效率
  • 便于单元测试

哪些项目不需要组件化

  • 项目较小,模块之间交互简单、耦合少
  • 模块没有被多个外部模块引用,只是一个简单的小模块
  • 模块不需要重用,代码也很少被修改
  • 团队规模小

组件化分层设计

iOS 组件化方案总结

底层组件通常包括底层类库、分类、宏定义文件等

注意:组件化分层实际就是通过接口隔离实现分层之间的依赖关系,而且只能上层对下层依赖项⽬公共代码资源下沉,横向的依赖也最好下沉

cocoapods 创建私有库

私有库创建流程如下:

iOS 组件化方案总结

写个 Demo 说明一下具体流程

1、在终端上移动到你想创建pod库工程的路径,并执行pod库的创建。命令如下:

plz@plzdeMacBook-Pro:~ better$ cd desktop
plz@plzdeMacBook-Pro:desktop better$ pod lib create PrivateHelloWorld

iOS 组件化方案总结

回车之后,终端会询问几个问题

# 选择编程语言
What language do you want to use?? [ Swift / ObjC ]
> Objc  
# 在你的项目中是否创建一个demo工程,为了方便测试,我选择了Yes
Would you like to include a demo application with your library? [ Yes / No ]
 > Yes  
# 测试框架选择哪一个
Which testing frameworks will you use? [ Specta / Kiwi / None ]
 > None
#要不要做视图测试
Would you like to do view based testing? [ Yes / No ]
 > NO
# 类前缀名
What is your class prefix?
 > HG

iOS 组件化方案总结

Pod私有库创建成功。一般来说创建成功会自动打开项目

2、安装CocoaPods项目找到刚才创建的PrivateHelloWorld文件夹,点击入去,里面的目录如下

iOS 组件化方案总结

3、添加你要添加代码文件(复制粘贴)

iOS 组件化方案总结

我这里放了PrintHelloWorld文件 pod install一下

# 移到Example目录下
BetterdeMacBook-Pro:desktop better$ cd /Users/better/Desktop/PrivateHelloWorld/Example 
# 安装CocoaPods项目
BetterdeMacBook-Pro:Example better$ pod install --no-repo-update

iOS 组件化方案总结

先进去Example文件夹点击后缀为xcworkspace的文件打开项目,运行下是否成功。

4、编辑CocoaPods的配置文件(后缀名为podspec)

打开PrintHelloWorld文件夹就可以看到

可以有很多种编辑方式编辑这个文件,如Xcode、文本编辑器、Sublime Text系列、Atom
我是用文本编辑器打开,难看是难看点,但胜在够方便

iOS 组件化方案总结

s.version我习惯是0.0.1开始
s.summary需要改改,不然待会提交会报错
s.homepage这里随便写个网站都行,建议写项目的首页,但一定要改,不然默认的会报错,因为没有默认的网址

s.source需要填一个git地址的私有库,github收费的,我选择的gitLab

iOS 组件化方案总结

复制私有库地址,在.podspec文件内的s.source替换地址

配置完成了

再次移到我们的Example文件,pod更新一下

BetterdeMacBook-Pro:Example better$ pod update --no-repo-update

打开项目,看看是否成功了

5、添加PrintHelloWorld,运行测试打开项目,在BYViewController.m里面导入PrintHelloWorld.h文件

iOS 组件化方案总结

6、验证pod配置文件为了保证项目正确性,pod文件配置没问题,在提交之前,我们需要验证一下

用终端移到我们的项目路径

BetterdeMacBook-Pro:~ better$ cd /Users/better/Desktop/PrivateHelloWorld 

到这里,我们已经完成源码导入、验证项目是否能运行、pod配置文件本地验证了

7、项目发布,tag 0.0.1终端移到该项目文件下执行git的相关命令

# 添加远程地址,即上面创建码云项目的地址
BetterdeMacBook-Pro:PrivateHelloWorld better$ git remote add origin https://gitee.com/Better_Y/PrintHelloWorld.git
# 添加文件
BetterdeMacBook-Pro:PrivateHelloWorld better$ git add .
# 提交本地,并写描述
BetterdeMacBook-Pro:PrivateHelloWorld better$ git commit -a -m "第一次提交 版本为0.0.1"
# --allow-unrelated-histories
# git pull origin maste会失败 ,提示:fatal: refusing to merge unrelated histories
# 原因是远程仓库origin上的分支master和本地分支master被Git认为是不同的仓库,所以不能直接合并,需要添加 --allow-unrelated-histories

BetterdeMacBook-Pro:PrivateHelloWorld better$ git pull origin master --allow-unrelated-histories
# 推送到码云的PrintHelloWolrd项目的master分支上
BetterdeMacBook-Pro:PrivateHelloWorld better$ git push origin master
# 提交版本号
BetterdeMacBook-Pro:PrivateHelloWorld better$ git tag 0.0.1
# push到远程分支
BetterdeMacBook-Pro:PrivateHelloWorld better$ git push origin 0.0.1

8、创建Sepc管理库 创建步骤跟上面码云创建的git私有库同理

在终端执行Specs创建命令

BetterdeMacBook-Pro:PrivateHelloWorld better$ pod repo add PrintSpecs https://gitlab.com/peiliuzhen/testspecs.git 

现在,我们可以直接发布了

# PrintSpecs是刚才上面添加的管理库名字 
# PrivateHelloWorld.podspec是PrintHelloWorld项目里面后缀为podspec的文件名
BetterdeMacBook-Pro:PrivateHelloWorld better$ pod repo push PrintSpecs PrivateHelloWorld.podspec

发布成功后,我们可以去码云看看PrivateSpecs的git项目有没有提交成功

iOS 组件化方案总结

到这里,我们的私有库发布已经全部完成了

9、检验私有库发布

新建privateDemo项目,创建Podfile文件并安装
Podfile代码如下

platform :ios,'8.0'
target 'privateDemo' do
pod 'PrintHelloWorld',:git => 'https://gitlab.com/peiliuzhen/privatehelloworld.git'
end

打开终端并执行pod安装指令:

BetterdeMacBook-Pro:~ better$ cd /Users/better/Desktop/privateDemo 
BetterdeMacBook-Pro:privateDemo better$ pod install --no-repo-update

我们打开PrivateDemo项目目录看看

iOS 组件化方案总结

到这里,验证我们的私有库发布就完满结束了!!

组件化基本操作

抽取解封装

使用一个常见的列表页举例说明:

iOS 组件化方案总结

cell的封装抽离:

iOS 组件化方案总结

组件化模块处理

模块耦合

iOS 组件化方案总结

模块解耦

iOS 组件化方案总结

组件通信

模块之间通信

iOS 组件化方案总结

开源组件化框架推荐(MJRouter、CTMediator、BeeHive)

目前主流的组件化方式有三种

  • URL路由 

  • target-action

  • protocol匹配

下面介绍一下三种方式对应的代表性框架

MJRouter(URL路由)

iOS常用路由工具一般都是使用URL匹配,或者根据约定的命名使用runtime方法进行动态调用。

这种实现方式优点是实现简单,缺点是需要维护字符串表,或者根据依赖命名约定无法在编译时暴露问题,运行时才发现错误。

URL 路由方式典型代表框架是蘑菇街开源的 MGJRouter,还有 JLRoutesHHRouter 等

MGJRouter 实现分析:

  1. App 启动时实例化各组件模块或者使用class注册,然后组件向ModuleManager注册Url
  2. 当组件A需要调用组件B时,向ModuleManager传递URL,参数可以拼接在URL后面或者放在字典里传递,类似 openURL。然后由ModuleManager负责调度组件B,最后完成目标

注册及调用代码如下:

// 1、注册某个URL[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]); }]; //2、调用路由[MGJRouter openURL:@"mgj://foo/bar"]; [MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]); // @{@"user_id": @1900} }]; [MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil]; 

URL 路由的优点

  • 具有很高的动态性,适合经常开展运营活动的app,例如蘑菇街自身属于电商行业

  • 方便统一管理多平台的路由规则

  • 易于适配 URL Scheme

URl 路由的缺点

  • 传参方式有限,并且无法利用编译器进行参数类型检查,因此所有的参数都是通过字符串转换而来

  • 只适用于界面模块,不适用于通用模块

  • 参数的格式不明确,是个灵活的 dictionary,也需要有个地方可以查参数格式。

  • 不支持storyboard

  • 依赖于字符串硬编码,难以管理,蘑菇街做了个后台专门管理。

  • 无法保证所使用的的模块一定存在

  • 解耦能力有限,url 的”注册”、”实现”、”使用”必须用相同的字符规则,一旦任何一方做出修改都会导致其他方的代码失效,并且重构难度大

CTMediator(target-action)

target-action 方案是基于OC的runtime、category特性动态获取模块,例如通过NSClassFromString获取类并创建实例,通过performSelector + NSInvocation动态调用方法,典型代表框架是 CTMediator

实现分析:

  1. 利用分类为路由添加新接口,在接口中通过字符串获取对应的类

  2. 通过runtime创建实例,动态调用实例的方法

页面间跳转实现代码如下:

//******* 1、分类定义新接口
#import "CTMediator+A.h"

@implementation CTMediator (A)

- (UIViewController *)A_Category_Swift_ViewControllerWithCallback:(void (^)(NSString *))callback
{
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"callback"] = callback;
    params[kCTMediatorParamsKeySwiftTargetModuleName] = @"A_swift";
    return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO];
}

- (UIViewController *)A_Category_Objc_ViewControllerWithCallback:(void (^)(NSString *))callback
{
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    params[@"callback"] = callback;
    return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO];
}
@end

//******* 2、模块提供者提供target-action的调用方式(对外需要加上public关键字)
#import "Target_A.h"
#import "AViewController.h"

@implementation Target_A

- (UIViewController *)Action_Category_ViewController:(NSDictionary *)params
{
    typedef void (^CallbackType)(NSString *);
    CallbackType callback = params[@"callback"];
    if (callback) {
        callback(@"success");
    }
    AViewController *viewController = [[AViewController alloc] init];
    return viewController;
}
- (UIViewController *)Action_Extension_ViewController:(NSDictionary *)params
{
    typedef void (^CallbackType)(NSString *);
    CallbackType callback = params[@"callback"];
    if (callback) {
        callback(@"success");
    }
    AViewController *viewController = [[AViewController alloc] init];
    return viewController;
}

//******* 3、使用
// Objective-C -> Category -> Objective-C
UIViewController *viewController = [[CTMediator sharedInstance] A_Category_Objc_ViewControllerWithCallback:^(NSString *result) {
    NSLog(@"%@", result);
}];
[self.navigationController pushViewController:viewController animated:YES];


优点

  • 利用 分类 可以明确声明接口,进行编译检查

  • 实现方式轻量

缺点

  • 需要在mediatortarget中重新添加每一个接口,模块化时代码较为繁琐

  • category 中仍然引入了字符串硬编码,内部使用字典传参

  • 无法保证使用的模块一定存在,target在修改后,使用者只能在运行时才能发现错误

  • 可能会创建过多的 target 类

CTMediator 核心源码分析

通过分类中调用的performTarget来到CTMediator中的具体实现,即performTarget:action:params:shouldCacheTarget:,主要是通过传入的name,找到对应的targetaction

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    //在swift中使用时,需要传入对应项目的target名称,否则会找不到视图控制器
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target 生成target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        //swift中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        //OC中target文件名拼接
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    //缓存中查找target
    NSObject *target = [self safeFetchCachedTarget:targetClassString];
    //缓存中没有target
    if (target == nil) {
        //通过字符串获取对应的类
        Class targetClass = NSClassFromString(targetClassString);
        //创建实例
        target = [[targetClass alloc] init];
    }

    // generate action 生成action方法名称
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    //通过方法名字符串获取对应的sel
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    //是否需要缓存
    if (shouldCacheTarget) {
        [self safeSetCachedTarget:target key:targetClassString];
    }
    //是否响应sel
    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 NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            @synchronized (self) {
                [self.cachedTarget removeObjectForKey:targetClassString];
            }
            return nil;
        }
    }
}

进入safePerformAction:target:params:实现,主要是通过invocation进行参数传递+消息转发

- (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];
    //void类型
    if (strcmp(retType, @encode(void)) == 0) {
        ...
    }
    //...其他类型可以阅读CTMediator源码
}

BeeHive(protocol class)

protocol匹配的实现思路是:

  • 1、将protocol和对应的进行字典匹配

  • 2、通过用protocol获取class,在动态创建实例

protocol比较典型的三方框架就是 阿里的BeeHiveBeeHive借鉴了Spring Service、Apache DSO的架构理念,采用AOP+扩展App生命周期API形式,将业务功能基础功能模块以模块方式以解决大型应用中的复杂问题,并让模块之间以Service形式调用,将复杂问题切分,以AOP方式模块化服务。

BeeHive 源码还没有深入探究,想了解的可以参考链接: BeeHive — 一次iOS模块化解耦实践

附参考文章:

打造完备的iOS组件化方案:如何面向接口进行模块解耦

今天的文章iOS 组件化方案总结分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/21620.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注