移动端模块化架构设计

这篇文档以 iOS 开发为例,老项目开发中一般存在一些弊端:

耦合严重,互相引用,状态无管理随处可修改
耦合强导致的冲突问题不好管理
实现较臃肿,各种功能类的代码混合在一起
无法快速响应业务变化,比如添加或者删除一个功能,影响面较广
重复的功能实现,无复用且影响包大小
内存及多线程问题

所以,需要一个架构来保证稳定高效的协同开发,对代码进行有效的复用,有助于持续集成及按需集成,同时也能保障代码质量,减少开发和维护成本。

设计

业务层

抽象及路由

现在大部分视图以 ViewController 为载体,相互引用调用,甚至在自己的 ViewController 修改其他 ViewController 状态。抽象成模块后,各个业务模块确立明确边界,每个模块抽象成一个 Module 对象遵循 ModuleDelegate 协议来控制其生命周期。应用 Bundle 内会维护一个 plist 存放映射关系,启动时会加载进内存,当该模块需要启动时,通过路由找到相应模块,反射实例化该模块对象并加载启动。

基础框架中会有一个 Router 对象来支持模块间的相互调用:

这样,各个业务模块就无需互相依赖,外界无法修改模块内状态,而是统一通过接口调用了。

参数传递时,只通过字符串的方式可能会有一些问题,比如:

  1. 参数 value 中包含复杂符号,可能需要 URLEncode/URLDecode
  2. 一些通用的复杂对象无法传递,比如 UIImage, 字典数组等

所以,也会提供类似 startModule:withLaunchOptions: 这样的接口,通过一个字典将对象传递给其他模块。

[someModule startModule:"User" withLaunchOptions:@{
    @"id":"123",
    @"type":"0",
    @"image":[UIImage imageNamed:@"avatar"]
}];

之所以也接受有 URL 字符串的接口,也是为了方便外部应用开启以及远程推送消息的模式,在主 AppDelegate 过滤掉部分外部应用后,可以直接按照约定格式的参数传递来打开到相应业务模块的指定页面。

还有一种情况是需要传递一个复杂的 model 到另一个模块,如果直接传递该 model 可能需要另外一个模块也引用该数据结构的头文件,这样会破坏模块独立的设计。基础框架会提供接口方法,提供对象、字典以及 JSON 格式的相互转换:

也可以通过类似 toDictionary:specifyKeys: 的接口指定关注的 key/value 生成子集字典。

如果两个模块间真的要传递或共享某个数据对象,可以两个模块各定义一份数据对象声明。如果这个对象是一个通用对象比如 user info 这种,也可以下沉到底层声明。

业务模块

如图所示,每个 biz 的模块可以自己组织模块结构,无论是 MVC, MVVM, MVP 等皆可。其中 Utils 中的工具代码,如果比较常用的可以下沉到基础框架中。

上文提到过,每个业务模块通过实现一个 delegate 并注册到全局的模块列表中来开放接口,下面来看下其生命周期:

其中比较特殊的是 moduleShouldStartMultWithLaunchOptions:launchMode:source: 方法,因为应用中可能会有一种场景,比如 A 模块打开 B 模块,然后 B 模块某个逻辑执行完又需要打开 A 模块,这个方法就决定了模块栈里允不允许多个同一类型模块存在。

业务模块往往还会有一些预加载的需求,比如提前下载一些数据,图片等,这时就需要有在模块启动前执行代码的能力,这时同样可以通过遵守 service delegate 来创建一个 service 类,应用 bundle 中同样会维护一个配置表 plist,应用启动时 context 会启动这些 Service 并由 service manager 管理。

其他层级

Runtime

理论上,应用 Context 是这个层级唯一向业务模块开放接口的类,提供启动/退出模块,获取当前模块等方法,而其实际则是内部调用 Router, Module Manager, Service Manager 的接口实现。Router 负责参数的解析,获取指定模块类并进行路由。 Module/service manager 则是分别维护模块和服务的配置表,内存中运行的模块和服务实例等。除了行为外,Context 的属性为只读,业务方不允许添加、修改全局 Context 的状态。

Component

Component 这里定义为既拥有 UI 展示能力,又不依赖于某个业务模块的组件,比如通讯录组件,Webview 容器,图片选择器等等。

Monitor

Monitor 作为应用体验很重要的一个部分,直接能够量化的反映应用当前以及隐藏的问题,它也可以细分为 Debug 和 Release 两个阶段。它提供的能力有:

监控程序中是否存在引用循环
帧数、内存、cpu 是否异常
UI 线程是否卡顿
是否有后台线程操作 UI
是否符合 UI design 原则
崩溃时获取 crash 堆栈
OC 代码使用问题
监控网络请求 request/response
其他性能指标

Library & Foundation

这里做为框架提供的基础能力,有的地方可能边界并不是那么清晰,比如 image 的第三方框架会自己写网络下载功能,那么它可能不会依赖于项目中的 network 的组件,有的 database 也会自己提供 data access object 能力。

工程化

当开发及维护一个项目的人员到达一定数量以后,就会出现代码合并效率降低,编译速度过慢,异地团队协作沟通成本增大,QA 测试回归灾难等等问题。这时候就需要将模块/组件工程化。

CocoaPods 是一个 iOS 端的包管理工具,能够将指定版本的目标集成到工程中,通过 CocoaPods 提供的功能,将每一个模块都单独作为一个工程,编译的目标是 framework ,本地开发时会 clone 下来一个壳工程,本地编写依赖的 Podfile 文件并执行 pod install 后便可进行开发。

这样每次编译就不需要全量编译所有文件,大大提升了开发效率。如果有多业务联调,依赖模块代码上传到服务器后,可以本地修改 Podfile 文件的版本号后重新执行 pod install 即可。

可能会遇到的问题

拆分粒度:

如果是旧工程,则可能遇到拆分粒度与难度的问题,如果拆分较细的话,灵活度比较高,粒度粗点则拆分难度会降低很多。而且每个人对业务模块的理解不同导致粒度上也需要讨论,比如假设有个 Product 模块,它可能包括产品分类,产品列表,产品详情等等,但也有人觉得产品详情页好多地方都在用,考虑单独拆分成单独的小模块。

工程化:

由于依赖的模块或者组件是通过 framework 引入的,可见的只有接口文件,这种黑盒情况下,出现问题排查起来就增加了难度,有返回输出的函数还好,可以看到输出的值或者类型是否符合期望,但如果是某种行为,或者底层静默的行为对上层造成了影响,查找问题的难度就增加了很多。

另外,维护 pods 也需要成本,比如一个基础组件的 api 进行了不得已的接口修改/升级,那么某个模块开发需要等所有依赖该基础组件的模块(包括壳工程)进行修改、提交后,才能编译通过。还有依赖问题,一般 pod 库都是通过 podspec 文件设置依赖,但是由于有时并不会很认真的分析和维护该文件,导致递归分析依赖时间过长,pod update 一直卡在 Analyzing Dependency 环节。

多端

进行模块化开发可以更方便代码及模块的复用,如果有新的项目还可以直接将抛去 biz layer 的所有基础框架直接转移到新的项目中去,减少重复开发并且能推进基础框架的不断迭代。

安卓在架构设计上同样可以采用相同的思路,实现细节和工程化实践方面会不同,但模块名称对应模块的配置表,一些服务的配置表双端一定要保持一致,方便以后业务的维护及新功能开发。

-- EOF --
以上就是这篇文章全部内容,欢迎提出建议和指正,个人联系方式详见关于

Comments
Write a Comment