序
《热爱生命》 — 汪国真
我不去想,
是否能够成功,
既然选择了远方,
便只顾风雨兼程。
我不去想,
身后会不会袭来寒风冷雨,
既然目标是地平线,
留给世界的只能是背影。
我不去想,
未来是平坦还是泥泞,
只要热爱生命,
一切 都在意料之中。
上世纪90年代初,“有华人的地方就有汪国真的诗”,他的诗创造了中国诗坛最后一个辉煌的时期。我觉得做人还是需要情怀和理想的,借用他的一句话:“我真羡慕少年,学什么都来得及,不像我们,总是感觉在被时间的鞭子抽打着走”。珍惜时间,常怀希望。
1.初衷
想把这几年做的事情记录下来,尽量详细,希望其他人读了这篇文章能够对项目组件化有一些借鉴意义。在改动业务框架遇到的问题,具体每个步骤都做哪些东西。做模块化不是为了做而做。如果没有业务场景需求,还是不建议做。
2.背景
自从入职至今,从最早期1.0版本到现在6.0版本,经历了包括在线找房、支付、签约、业务线扩充等各种大事记。
早期只有一个业务,业务也相对简单,一个Project,12万行代码,就可以解决问题。随着公司发展业务扩张,开发人员扩张到约有35人,还在增长。5条业务线。同时,这个Project增长到40+万行代码。
此时我们遇到了一些问题:
第一,代码冲突多。每次拉下代码开发功能准备提交代码时,往往有其他工程师提交了代码,一般合并代码的过程至少需要半个小时。
第二,运行调试速度慢。即使开发一个小的功能,也要在整个工程里做编译和调试,编译运行一次要10几分钟。效率非常低。
第三,程序员出错几率高。开发某个业务线功能的时候,经常因为代码冲突或者复用别人代码的原因,修改了其他业务线的代码,产生线上问题紧急发版。
第四,迭代速度慢。业务线和公共代码相互耦合,代码冲突较多;等快要发版时所有的业务线修改都需要全量回归,每次发版都比较晚。
第五,版本风险。由于是源码级集成,如果想增加或者撤回功能都有风险,都需要修改很多代码,风险不可控。
基于以上问题,团队技术方向也一直在变换,不断摸索解决之道。
3.历程
1.0
初创期业务单一,公司要求快速完成开发,占领市场。应用主要以功能为主,但是只有地图、列表、聊天、支付等一些基本功能。基于产品业务的设计,3-5个人小分队,完成了当时框架的雏形。
框架上设计考虑了纵向划分,对于网络层,数据层,逻辑层,Ui层分别进行了解耦。后来发现,没有严格的模块隔离导致很难升级模块,所以通过中间件对每个模块做接口隔离。
使用单一的项目,结构仍以功能为主,进行代码划分。做了纵向层级关系,隔离及解耦。架构设计原则上更多的还是考虑了稳定性和兼容性。对于一些共用的功能进行封装。
3.0
业务扩张,代码量增长,一下突破65536上限。出现代码冗余,各业务线UI不统一。此时急需要公共业务层支撑,并考虑到未来的发展,初步搭建架构小组,剥离公共代码,把项目以业务形式归类。为组件化、插件化奠定基础。
通过lib库的形式依赖底层技术层以及公共业务层代码,同时对业务线代码进行拆分解耦。这个阶段主要利用业务开发间隙,进行代码拆解,整合。对于冗余的代码进行清理,统一代码规范,统一UI。
自动化构建初版
困境,测试同事需要研发打一个指定环境的包来验证问题。开发同事提交代码、切换仓库的分支、切接口环境、切appKey配置、选择打包签名,开始打包 。cpu占用,环境运行缓慢,10多分钟什么也干不了, 打好包发送还被网络限速。假设每人每天要打5个包,每个包要耗费10分钟的开发时间,每个月大约有20个工作日,一年要花在打包上的时间有 10×5×20×12≈200H。
曙光,搭建Jenkins APP自动化构建系统非常必要。上线之后,找研发要一下git仓库分支名,就可以在自己本地去打包。要知道这个包里包含了哪些修改,去发起的构建任务的变更记录里一目了然。需要安装这个包,但又不会安装,打完包自动上传到小飞机,扫一下二维码即可下载安装。
|
构建参数名 |
参数含义 |
备注 |
|
BRANCH |
APP客端业务仓库 |
Git开发分支,找研发提供 |
|
BASE_BRANCH |
APP客端基础框架仓库 |
默认dev,如果有需求研发提供对应分支 |
|
COMMON_BRANCH |
APP客端业务框架仓库 |
默认dev,如果有需求研发提供对应分支 |
|
UPLOAD_FIR |
是否上传小飞机 |
|
|
FIR_LINE |
小飞机上传账号 |
|
|
JENKINS_ENV |
接口环境 |
color:测试+准生产 product:生产 |
|
APP_KEY_CONFIG |
三方SDK APPKEY配置 |
1:测试key 2:正式key |
|
assembleTask |
Apk签名 |
debug:测试apk签名 release:正式apk签名 |
|
BUILD_TYPE |
包类型 |
debug:测试 release:正式 |
|
BRANCH |
开发分支 |
默认develop |
|
UPLOAD_FIR |
是否上传小飞机 |
|
|
FIR_LOGINNAME |
小飞机账号 |
|
以上就是打包环境中需要的配置参数设置
6.0
业务扩展更大,代码膨胀,通过长期摸索,考虑组件化、热更新和插件化。组件化是在编译期分模块,插件化是在运行期。一般插件化用于动态修复bug或者动态更新模块,相对来说黑科技更多一些,而相对于插件化,组件化的架构更容易操作,效率高,基于安卓自有特性和gradle的功能,没有插件化那么多的坑,对于大多数应用而言,其优势还是相当明显的。经过两轮评审选定组件化,为未来各业务线的技术改造方案。
4.开始组件化之旅
4.1 原理与思路:
每一个module设置library和application两种属性,通过配置文件控制,当集合一起编译时作为library依赖到app管理系统,当独立开发时切换到application属性变成一个普通的module,可以独立运行。页面跳转基于ARouter路由框架。组件管理系统主要负责组件的添加、更新、删除。以aar形式封装组件,放入maven管理。组件分类为业务组件、公共组件、基础组件、三方组件。基础组件和三方组件向下沉,业务组件上升。业务组件以具体业务为主,可以独立编译打包。基础组件包括图片加载、网络等。公共组件包括ZUI,工具库等,ZUI提供通用布局,通用控件,工具组件注意context的传递。使用继承组件类的方式实现符合自身业务的控件。三方组件库注意三方API升级问题,使用中间层封装。组件层类似搭积木的形式为app层多个应用提供支持。
4.2 框架概览:
集合编译时项目是这样的:
main里面是应用首页,引导页,壳工程全局配置,wireless里面是组件编译时使用到的源码,jenkins中放置自动构建系统需要的环境配置脚本,gradle和properties在独立仓库,放置组件管理系统上传,获取,依赖所用的脚本。
独立编译时项目是这样的:
application文件夹存放作为应用时的Manifest文件,区别作为库时的Manifest文件。wireless里面是依赖组件编译时使用到的源码。在组件中创建application文件。
4.3 准备工作:
解耦时需要做的准备工作。因为这些工作是解耦拆分的基础。
1、工作量评估:难点是老业务和业务并行 ,业务需求跟拆分需求冲突 ,时间方案规划困难。首先是协调产品测试梳理现有业务流程,明确拆分方案 ,错峰进行拆分项目与业务需求的研发。
2、公共资源,公共model,utils,公共view等的拆分。虽然不用考虑太多事情,但是很繁琐。在做模块化的时候,这个地方最耗费时间。还有代码不够规范,对代码耦合不够敏感。例如:utils写在一个类里面。什么都向里面放。在传入的参数方面也不够规范,甚至业务代码作为参数传入。导致这个东西难拆。utils的方法不传context。使用项目中统一加的一个静态的context,导致几乎所有的utils都没有传入context,这些工具方法直接依赖主app。公共view 不独立。既然是common view,那么就让这个view能够独立,不要耦合其他第三方库。
3、数据库的抽离 网络数据使用文件存储且对业务代码透明。敏感数据使用数据库存储,但用接口隔离。
4.4 业务组件拆分:
4.4.1、资源拆分,资源的拆分非常繁琐。string, color,dimens这些资源分布在了代码的各个角落,一个个去拆,要撞墙了。为了避免组件之间资源命名重复,使用resourcePrefix约束资源前缀 。
开发工具用于给资源文件统一添加前缀名,同时会搜索引用资源的代码一并进行修改。
都会修改哪些文件?
- res下的所有文件的文件名(除了values和drawable*文件夹下的文件)
2. java和xml文件里的资源id引用,不会修改依赖库的资源id的引用
使用方法
java -jar FormatResourceName.jar <模块根路径> <前缀名> [-e]
|
参数 |
说明 |
|---|---|
|
参数 |
说明 |
| 模块根路径 |
|
| 前缀名 | 要添加的资源名前缀,如果资源名已经是以该前缀开头,则不会重复添加前缀 |
| -e | 如果没传入该参数,则不会真正执行重命名操作,而是把将要变化的内容输出到map.txt内,可供使用人员预览改动情况 |
修改效果截图
注意styles的坑。写属性名字的时候,一定要加上前缀限定词。
4.4.2、业务代码拆分
修改组件下的build.gradle文件
(1)组件状态控制逻辑
修改前:
修改后:
(2)组件Manifest文件路径声明
(3)修改声明的依赖库引用
在gradle目录initTPVersion.gradle文件中查找所需依赖库,若不存在,则根据命名规范,在文件中追加。
修改前:
修改后:
(4)若依赖于其它组件库、jar包、工程,声明方式如下:
(5)引入maven插件
(6)增加组件打包、上传代码
注:在gradle插件 3.0版本中,compile 指令被标注为过时方法,而新增了两个依赖指令,一个是implementation和api。implementation可以提升便以速度,依赖首先应该设置为implementation,如果有错,那么使用api指令。使用compile指令也没有问题。
(7) BuildConfig
build时候组件的包会生成BuildConfig被打成aar里面 ,不能随主工程的编译改变。如果想使用主工程的参数,例如jenkins选择参数的时候,应该通过application方式传递。
5. issues
5.1 ButterKnife的坑 android ADT14开始,library的R资源不再是final类型的了,ButterKnife在8.1.0后期及之后版本中已经对【在library的module中使用butterknife】提供了支持。
5.2 switch语句遇到的坑 一般我们在写代码时如果有多个if..else都喜欢用switch语句,尤其在Android中的onClick事件中,会处理多个控件的监听事件,通常在这时候使用switch都挺顺手的,代码简洁整齐。but,如果你在library module中这么写,可能会报一个这样的错误: Validates using resource IDs in a switch statement in Android library module….
因为switch里的case值必须是常数,而在library module的R文件里ID的值不是final类型的,但是主module的R文件里的ID值是final类型的,所以主module里可以用资源ID作为case值而library module却不能。在Android Studio中选中switch,按Option+Return(Mac),Alt+Enter(Windows),选择Replace ‘switch’ witch ‘if’即可。
5.3 混淆的坑
对于主APP工程和lib工程来说,在生成apk的时候是会自动合并的,这给我造成一个误区让我以为主工程里面配置的混淆和lib工程配置的混淆在生成apk的时候也会自动合并,经过测试发现,混淆配置是不会合并的。如果APP主工程中开启了混淆而lib工程的混淆配置没有开启,那么编译出来的apk,对于lib工程来说,包名,类的路径,类名都是没有混淆的,但是类里面的字段是有混淆的,也就是说lib工程有混淆,但是混淆的并不彻底,仅仅是类里面的字段进行了混淆,之所以这样的原因是,lib工程本身没有开启混淆,所以lib工程的proguard-rules.pro混淆配置并没有生效,而app主工程开启了混淆,所以app主工程的proguard-rules.pro混淆配置生效了,而且app主工程的混淆配置还对lib工程生效了,由于app主工程中的proguard-rules.pro并没有配置lib工程的混淆内容,所以lib工程代码就会被混淆了。所以bean里面直接用public字段方式,没有用get set方法。会解析找不到变量名崩溃。
方案是,在lib工程中加入consumerProguardFiles ‘proguard-rules.pro’,这个配置会在lib工程被打包的时候让lib工程中的proguard-rules.pro生效,此时lib工程就会按照它自己的proguard-rules.pro文件配置被正确混淆(这种相当一对多个工程的混淆配置文件进行merge)所以当项目发布的时候最正确和高效的配置:app主工程开启混淆,配置好app自己的混淆配置文件,lib工程同时也开启混淆,并配置自己的混淆配置文件,并且记得在lib工程里面加上consumerProguardFiles ‘proguard-rules.pro’,这样app和lib最终都能得到正确的混淆代码。
5.4 广播和eventbus
大量的使用eventbus,有大量的onEventMainThread方法,阅读起来很痛苦。如果项目中发送这个Event的地方非常多,接收这个Event的地方也很多。在进行代码拆分时,不敢轻举妄动,生怕哪些事件没有被接收。广播和eventbus类似,如果项目中存在同一事件的大量发送和接收,那么项目的可读性和可维护性就会变得相当差。关于模块间/页面间通信互动,进程通信,在后面深入探讨。
6. 后记
流程大体写完了,篇幅时间有限,无法详细的面面俱到。只是当成自己的一个经验感悟吧。组件化是一个繁琐,枯燥,耗费时间长,做了大量的工作,但是在 表面功能上,别人都看不到。不如花一点时间,引入一个第三方库看着花哨。很大一部分工作量是为以前欠设计的代码逻辑买单。随着公司规模的扩张,业务发展,进行模块化是必然的事情。
7. 参考资料
猫眼组件化实践
滴滴组件化实践优化
美团架构设计