iOS 模块化进阶整理记录

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

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

  • 编译加快ios

    在原模式中,须要 150s 左右整个编译完毕,而后开发人员才能够开始调试。而如今组件化以后,某个业务组件只须要 10s ~ 20s 左右便可开工git

  • 结合 MVVMgithub

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

  • 回滚更方便ruby

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

  • 上面都是忽悠你来看的,别当真 🤣网络

模块化抽离

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

1)初级封装抽离:

主要工做就是把 App 之间重用的 Util、Category、网络层和本地存储等抽成了 Pod 库,因为三方库自带必定的解耦性,对后期的组件化开发也比较有帮助。另外一方面工做好比Chart,ChartSocket这些功能在各个 App 之间重用的却不会过于耦合,因此拆分难度也不会过高。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)

相关文章
相关标签/搜索