iOS 模块化进阶整理记录

先说模块化可能给项目带来的改变:

  • 代码提交更规范,分工更为明确,质量提高

  • 编译加快

    在原模式中,需要 150s 左右整个编译完毕,然后开发人员才可以开始调试。而现在组件化之后,某个业务组件只需要 10s ~ 20s 左右即可开工

  • 结合 MVVM

    更加细化的单元测试,提高代码质量,保证 App 稳定性

  • 回滚更方便

    面对发生业务或者 UI 变回之前版本的情况,以前我们都是 checkout 出之前的代码。而现在组件化了之后,我们只需要使用旧版本的业务组件 Pod 库,或者在旧版本的基础上再发一个 Pod 库便可。

  • 上面都是忽悠你来看的,别当真 ?

模块化抽离

最近一直在调研模块化的相关知识,基本掌握了“初级封装抽离”的水平,正在迷茫之际,遇大神指点迷津,探索出了后面的进阶路线,心中默默感谢大神一刻钟...

1)初级封装抽离:

主要工作就是把 App 之间重用的 Util、Category、网络层和本地存储等抽成了 Pod 库,由于三方库自带一定的解耦性,对后期的组件化开发也比较有帮助。另一方面工作比如Chart,ChartSocket这些功能在各个 App 之间重用的却不会过于耦合,所以拆分难度也不会太高。

这一级的抽离相对简单,难点倒是对 cocopods 等工具的使用,目前的我组件化学习就只到这个水平,大家共同学习!

相关组件化工具的使用参考:《使用 CocoaPods 对公有库开源和私有库组件》https://juejin.im/post/5ab21daaf265da239e4df64e

都是自己摸着石头过河,有什么不对的地方,大家探讨哈~

2)中级解耦抽离:

以 Analytics 统计功能为例,Analytics 是依赖 UMengAnalytics 来做统计的,用于收集数据的方法处理不好极易发生耦合,如既依赖了 User,还依赖了 currentServerId等。

应对 Analytics 这类情况,网上资料有几种方法来解耦:

  • 1.把它依赖的代码先做成一个 Pod 库,然后转而依赖 Pod 库。有点像是“依赖下沉”。
  • 2.使用 category 的方式把依赖改成组合的方式。
  • 3.使用一个 block 或 delegate(协议)把这部分职责丢出去。
  • 4.直接 copy 代码,其实我首先想到的就是这个 ?,copy 代码这个事情看起来很不优雅,但是它的好处就是快。对于一些不重要的工具方法,也可以直接 copy 到内部来用。

对于解耦,网上类似的资料还有利用中间件 Mediator的方式:

应对上面的情景,最直接的方法就是增加一个中间件,各个模块跳转通过中间件来管理。这样,所有模块只依赖这个中间件。

但是中间件怎么去调用其他模块那?好吧,中间件又会依赖所有模块。好像除了增加代码的复杂度,并没有真正解决任何问题。

有没有一种方法,可以完美的解决这个依赖关系那?

我们希望做到:每个模块之间互相不依赖,并且每个模块可以脱离工程由不同的人编写、单独编译调试。

下面的方案通过对中间件的改造,很好的解决了这个问题,解决后的模块间依赖关系如下:

实现方案 demo 源码地址: https://github.com/zcsoft/ZC_CTMediator,搞来学习吧

目录结构:

所有模块的引用关系如图:

由于 demo 中只是从 ViewController.h.m 中跳转到 DemoModule 模块,所以只需要 ViewController.h.m 依赖 CTMediator,CTMediator 到 DemoModule 模块的调用是使用运行时完成了(图片中的蓝线),在代码中不需要相护依赖。

也就是说,如果一个模块不需要跳转到其他模块,就不需要依赖 CTMediator。

完整的内部调用关系图:

响应过程:

1.ViewController 中判断Cell选中
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
-> 
2.CTMediator+CTMediatorModuleAActions 中图片加载响应方法
- (void)CTMediator_presentImage:(UIImage *)image;
-> 
3.CTMediator 中本地组件调用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
->
4.Target_A
- (id)Action_nativePresentImage:(NSDictionary *)params;
完成跳转。
复制代码

事件响应断点:

Demo 中各个文件功用说明:

<1>CTMediator.h.m 功能: 指定目标(target,类名)+动作(action,方法名),并提供一个字典类型的参数。

CTMediator.h.m 会判断 target-action 是否可以调用,如果可以,则调用。由于这一功能是通过 runtime 动态实现的,所以在 CTMediator.h.m 的实现中,不会依赖任何其他模块,也不需要知道 target-action 的具体功能,只要 target-action 存在,就会被执行 target-action 具体的功能由 DemoModule 自己负责)。

CTMediator.h 里实际提供了两个方法,分别处理 url 方式的调用和 target-action 方式的调用,其中,如果使用 url 方式,会自动把 url 转换成 target-action。

<2>CTMediator+CTMediatorModuleAActions.h.m 功能:CTMediator 的扩展,用于管理跳转到 DemoModule 模块的动作。其他模块想要跳转到 DemoModule 模块时,通过调用这个类的方法来实现。

但是这个类中,并不真正去做跳转的动作,它只是对 CTMediator.h.m类的封装,这样用户就不需要关心使用CTMediator.h.m跳转到DemoModule模块时具体需要的target名称和action名称了。

<3>‘CTMediator.h.m’+‘CTMediator+CTMediatorModuleAActions.h.m’ 共同组成了一个面相 DemoModule 的跳转,并且它不会在代码上依赖 DemoModule,DemoModule 是否提供了相应的跳转功能,只体现在运行时是否能够正常跳转。

至此,CTMediator 这个中间层实现了完全的独立,其他模块不需要预先注册,CTMediator也不需要知道其他模块的实现细节。唯一的关联就是需要在 ‘CTMediator+CTMediatorModuleAActions.h.m’ 中写明正确的 target+action 和正确的参数,而且这些 action 和参数只依赖于 Target_A.h.m。

action 和参数的正确性只会在运行时检查,如果 target 或 action 不存在,可以在 ‘CTMediator.h.m’ 中进行相应的处理。既:CTMediator 不需要依赖任何模块就可以编译运行。

<4>Target_A.h.m 提供了跳转到 DemoModule 模块的对外接口,与 CTMediator+CTMediatorModuleAActions.h.m 相互对应,可以说它只用来为 CTMediator+CTMediatorModuleAActions.h.m 提供服务,所以在实现 CTMediator+CTMediatorModuleAActions.h.m时只需要参考 TargetA.h.m 即可,足够简单以至于并不需要文档来辅助描述。其他模块想跳转到这个模块时,不能直接通过 Target_A.h.m 实现,而是要通过 CTMediator+CTMediatorModuleAActions.h.m 来完成。

这样,就实现了模块间相互不依赖,并且只有需要跳转到其他模块的地方,才需要依赖 CTMediator。

<5>DemoModuleADetailViewController.h.m DemoModule 模块的主视图,这个例子中,会从 ViewController.h.m 跳转到这个模块。

<6>AppDelegate.h.m APP 入口,从应用外通过 Scheme 跳入程序时会经过这个类。

<7>ViewController.h.m APP 主视图,需要在这里跳转到 DemoModule 模块。

3)高级初始化抽离:

AppDelegate 充斥着各种初始化和第三方的注册,这些初始化会被各个业务组件使用,而且第三方库基本都需要注册一个 AppKey ,特别是一些第三方的库需要在 application: didFinishLaunchingWithOptions: 时初始化。

面对这种高难度的耦合场景,我想到了一个基于 runtime 的 AOP 解决方案。

关于AOP的简单介绍参考: 《基于 Aspects 简单展示 AOP 面向切面编程(中英文)》https://juejin.im/post/5a7abf495188257a61322204

原理就是利用 runtime,不需要在 AppDelegate 中添加任何代码,就可以捕获 App 生命周期,具体的解决方案还有待探讨。

这里引用《iOS App组件化开发实践》的解决方案,通过创建一个 PBBasicProviderModule 弱业务组件:

  • 它通过依赖YTXModule来捕捉App生命周期。
  • 它来负责初始化自己的和第三方的东西。
  • 所有业务组件都可以依赖这个弱业务组件。
  • 它来保证所有东西一定是是初始化完毕的。
  • 它来统一管理。
  • 它来暴露一些类和功能给业务组件使用。
什么是业务组件和弱业务组件?

业务组件里面基本都有:storyboard、nib、图片等等。弱业务组件里面一般没有。这不是绝对的,但一般情况是这样。 业务组件一般都是App上某一具体业务。比如首页、我、直播、行情详情、XX交易大盘、YY交易大盘、XX交易中盘、资讯、发现等等。而弱业务组件是给这些业务组件提供功能的,一般自己不直接表现在App上展示。

代码截取:

@implementation PBBasicProviderModule

YTXMODULE_EXTERN()
{

}

+ (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(nullable NSDictionary *)launchOptions
{
  [self setupThirdParty:application didFinishLaunchingWithOptions:launchOptions];
  [self setupBasic:application didFinishLaunchingWithOptions:launchOptions];

  return YES;
}

+ (void) setupThirdParty:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
  [self setupEaseMob:application didFinishLaunchingWithOptions:launchOptions];
  [self setupTalkingData];
  [self setupAdTalkingData];
  [self setupShareSDK];
  [self setupJSPatch];
  [self setupUmeng];
// [self setupAdhoc];
  });
}

+ (void) setupBasic:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [self registerBasic];

  [self autoIncrementOpenAppCount];

  [self setupScreenShowManager];

  [self setupYTXAnalytics];

  [self setupRemoteHook];
}

+ (YTXAnalytics) sharedYTXAnalytics
{
  return ......;
}
......
复制代码

《iOS App组件化开发实践》介绍的层级结构设计图:

《iOS App组件化开发实践》推行的组件化规范:

  • 业务组件之间不能有依赖关系。
  • 按照图示不能跨层依赖。
  • 所谓弱业务组件就是包含着少部分业务,并且可以在这个App内的各个业务组件之间重用的代码。
  • 要依赖YTXModule的组件一定要以Module结尾,而且它一定是个业务组件或是弱业务组件。
  • 弱业务组件以App代号开头(比如PB),以Module结尾。例:PBBasicProviderModule。
  • 业务组件以App代号开头(比如PB)BusinessModule结尾。例:PBHomePageBusinessModule。
  • 业务组件之间不能有依赖关系,这是公认的的原则。否则就失去了组件化开发的核心价值。

由于引入PBBasicProviderModule解决AppDelegate中的各种问题,会导致PBBasicProviderModule体量激增,以下是《iOS App组件化开发实践》中的解决方案。

据说美团的组件化开发必须依赖主App的AppDelegate的一大堆设置和初始化。所以干脆他们就直接在主App中集成调试,他们通过二进制化和去Pod依赖化的方式让主App的构建非常快。

所以我们是不是可以继续污染这个PBBasicProviderModule。不需要在主App项目里的AppDelegate写任何初始化代码?基本或者尽量不在主App里写任何代码?改依赖主App变为依赖这个弱业务组件?

按照这个思路我们搬空了AppDelegate里的所有代码。比如一些初始化App样式的东西、初始化RootViewController等等这些都可以搬到一个新的弱业务组件里。

而业务组件其实根本不需关心这个弱业务组件,开发人员只需要在业务组件中的Example App中的AppDelegate中初始化自己业务组件的RootViewController就好了。

其他的事情交给这个新的弱业务组件就好了。而主App和Example App只要在Podfile中依赖它就好了。

所以最后的设想就是:开发者不会去改主App项目,也不需要知道主App项目。对于开发者来说,主App和业务组件之间是隔绝的。

上面这些表示一脸懵逼,来源下面有地址,大家自行理解。

坑点之 Debug/Release:

在对二进制Pod库跑测试的发现,源码能过,二进制(.a)不能过。 问题源头(这是二进制化的锅):

#ifdef DEBUG

#endif
复制代码

由于DEBUG在编译阶段就已经决定了。二进制化的时候已经编译完成了。

解决方案:

创建了一个 PBEnvironmentProvider 大家都去依赖它。

然后原来判断宏的代码改成这样:

if([PBEnvironmentProvider testing])
{
//...
}
复制代码

在主App的AppDelegate中这样:

#if DEBUG && TESTING
//PBEnvironmentProvider提供的宏
CONFIG_ENVIRONMENT_TESTING
#endif
复制代码

原理是: 如果AppDelegate有某个方法(CONFIG_ENVIRONMENT_TESTING宏会提供这个方法),[PBEnvironmentProvider testing]得到的结果就是YES。

业务组件间通信

App路由能解决哪些问题:

1)3D-Touch功能或者点击推送消息,要求外部跳转到App内部一个很深层次的一个界面。

2)自家的一系列App之间如何相互跳转?

3)如何解除App组件之间和App页面之间的耦合性?

4)如何能统一iOS和Android两端的页面跳转逻辑?甚至如何能统一三端的请求资源的方式?

5)如果使用了动态下发配置文件来配置App的跳转逻辑,那么如果做到iOS和Android两边只要共用一套配置文件?

6)如果App出现bug了,如何不用JSPatch,就能做到简单的热修复功能?

7)如何在每个组件间调用和页面跳转时都进行埋点统计?每个跳转的地方都手写代码埋点?利用Runtime AOP ?

8)如何在每个组件间调用的过程中,加入调用的逻辑检查,令牌机制,配合灰度进行风控逻辑?

9)如何在App任何界面都可以调用同一个界面或者同一个组件?只能在AppDelegate里面注册单例来实现?

App之间跳转实现

1)URL Scheme方式 2)Universal Links方式

组件间通信的三方库支持也有许多如:
  • 1.主流的有类似JLRoutes,主打通过URL跳转协议(https://github.com/joeldev/JLRoutes)
  • 2.HHRouter:这是布丁动画的一个Router,灵感来自于 ABRouter 和 Routable iOS。
  • 3.美丽联合开源的三方库MGJRouter(https://github.com/meili/MGJRouter),使用项目包括旗下的:蘑菇街、美丽说等。

关于JLRoutes简单介绍:《iOS 模块化之 JLRoute 路由示例 (中英文)》(https://github.com/ReverseScale/JLRouteDemo)

搬运来的一些注意事项:

1.页面跳转

页面跳转解决方案与业务组件之间通信问题是一样的。

但是需要注意的是,你一个业务组件内部的页面跳转也请使用URL+Router的方式跳转,而不要自己直接pushViewController。

这样的好处是:如果将来某些内部跳转页面需要给其他业务组件调用,你就不需要再注册个URL了。因为本来就有。

2.是否去Model化

去Model化主要体现在业务组件间通信,要不要传一个Model过去(传过去的Dictionary中的某个键是Model)。

如果去Model化,这个业务组件的开发者如何确定Dictionary里面有哪些内容分别是什么类型呢?那需要有个地方传播这些信息,比如写在头文件,wiki等等。

如果不去Model化的话,就需要把这个Model做成Pod库。两个业务组件都去依赖它。

最后决定不去Model。因为实际上有一些Model就是在各个业务组件之间公用的(比如User),所以肯定就会有Model做成Pod库。我们可以把它做成重Model,Model里可以带网络请求和本地存储的方法。唯一不能避免的问题是,两个业务组件的开发者都有可能去改这个Model的Pod库。

3.信息的披露

不同业务开发者如何知晓这些信息。 使用去Model化和不使用去Model化,我们都有各自的方案。 去Model化,则披露头文件,在头文件里面写详细的注释。

如果不去Model化,则就看Model就可以了。如有特殊情况,那也是文档写在头文件内。 总结的话:信息披露的方式就是把注释文档写在头文件内。

4.组件的生命周期

业务组件的生命周期和App一样。它本身就是个类,只暴露类方法,不存在需要实例,所以其实不存在生命周期这个概念。而它可以使用类方法创建很多ViewController,ViewController的生命周期由App管理。哪怕这些ViewController之间需要通信,你也可以使用Bus/YTXModule/协议等等方式来做,而不应该让业务组件这个类来负责他们之间的通信;也不应该自己持有ViewController;这样增加了耦合。

弱业务组件的生命周期由创建它的对象来管理。按需创建和ARC自动释放。 基础功能组件和第三方的生命周期由创建它的对象来管理。按需创建和ARC自动释放。

5.版本规范

所有Pod库都只依赖到minor

"~> 2.3"
复制代码

主App中精确依赖到patch

"2.3.1"
复制代码

主App中的业务组件版本号的Main.Minor要和主App版本保持一致。

参考: Semantic Versioning(https://semver.org), RubyGems Versioning Policies(http://guides.rubygems.org/patterns/#semantic-versioning)

6.单元测试

单元测试我们用的是 Kiwi 。 结合MVVM模式,对每一个业务组件的ViewModel都进行单元测试。每次push代码,gitlab-runner都会自动跑测试。一旦开发人员发现测试挂了就能够及时找到问题。也可以很容易的追溯哪次提交把测试跑挂了。

7.持续集成

原来的App就是持续集成的。想当然的,我们希望新的组件化开发的App也能够持续集成。 Podfile应该是这样的:这里面出现的全是私有Pod库。

pod 'YTXRequest', '2.0.1'
pod 'YTXUtilCategory', '1.6.0'

pod 'PBBasicProviderModule', '0.2.1'
pod 'PBBasicChartAndSocketModule', '0.3.1'
pod 'PBBasicAppInitModule', '0.5.1'
...

pod 'PBBasicHomepageBusinessModule', '1.2.15'
pod 'PBBasicMeBusinessModule', '1.2.10'
pod 'PBBasicLiveBusinessModule', '1.2.1'
pod 'PBBasicChartBusinessModule', '1.2.6'
pod 'PBBasicTradeBusinessModule', '1.2.7'
...
复制代码

持续集成(工具:gitlab runner)的整个流程是:

第一步:

使用template创建Pod。像这样:

pod lib create <Pod库名称>

--template-url="http://gitlab.baidao.com/pods/ytx-pod-template"
复制代码

第二步:

创建dev分支。用来开发。

第三步:

每次push dev的时候会触发runner自动跑Stage: Init Lint(中的test)

第四步:

1.准备发布Pod库。修改podspec的版本号,打上相应tag。

2.使用merge_request.sh向master提交一个merge request。

第五步:

1.其他有权限开发者code review之后,接受merge request。

2.master合并这个merge request 3.master触发runner自动跑Stage: Init Package Lint ReleasePod UpdateApp

第六步:

如果第五步正确。主App的dev分支会收到一个merge request,里面的内容是修改Podfile。 图中内容出现了AFNetworking等是因为这个时候在做测试。

第七步:

主App触发runner,会构建一个ipa自动上传到 fir 。

以上注意内容来自:https://blog.csdn.net/u013602835/article/details/52668894,还没机会实践,先存着


参考来源

本文整理内容参考了以下文章,在此对原作者们表示感谢:

  • 《iOS应用架构谈 组件化方案》(https://casatwy.com/iOS-Modulization.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io)

  • 《路由设计思路分析》(https://juejin.im/post/58b2aad6b123db0052cc9edd)

  • 《iOS 组件化方案探索》(http://blog.cnbang.net/tech/3080/)

  • 《iOS App组件化开发实践》(http://www.infoq.com/cn/articles/ios-app-component-development-practice)

  • 《iOS 业务模块间互相跳转的解耦方案》(https://blog.csdn.net/cuibo1123/article/details/51017376)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值