Android容器化方案
撰写日期:2017-6-1 作者:庄文志
项目背景
项目在初期,因为功能少,项目紧张,我们只会在耦合度较高的情况下进行开发,各功能的耦合上如下:
1)在这种情况下,各个模块之间耦合度高,各模块之间缺一不可,删掉某一个模块,或者重用某一个模块,需要动用大量的代码,而且移植完之后,还要进行大量的测试,才能保证移除不出问题。
2)代码逻辑难以测试(一些逻辑在界面层),新员工上手需要了解各个模块的功能,才能确保自身代码不影响其他模块。
3)较多代码时,编译较慢,需要调试一些模块的时候,需要从界面上从第一层进入到最后一层。
综合以上总结,我们可以总结出我们碰到的问题
1,单个app编译很慢,虽然instant run的功能加快的编译
2,机型的不同,实现的方式也各自略有不同。相同的api,由于机型的差异实现不同
3,人员多,开发的技术不同
4,功能多,各个功能间耦合度高
5,线上的发布,需要手动推送提示用户更新,体验性差,覆盖率慢。
为什么要做组件化,容器化等技术
容器化是什么?容器化,就像一张桌子,你在上面放什么桌子不关心你放了什么东西,他只关心,我的桌面是不是平的,桌脚占得稳不。而组件,就是这桌子上的一个个物件,比如一个杯子,若干书本,一些笔……等等,他们都是组件,容器给了他们一个支撑,一个平面,放着什么东西不重要,重要的是容器是否足够大,是否容得下。
但是现在有很多插件化,容器化的技术。插件化,容器化比较有代表性的如下:
Qihoo360/DroidPlugin
CtripMobile/DynamicAPK
mmin18/AndroidDynamicLoader
singwhatiwanna/dynamic-load-apk
houkx/android-pluginmgr
bunnyblue/ACDD
wequick/Small
alibaba/Altas
插件化的技术由来已久。插件化的原理基本靠hook各种底层服务的API,例如ActivityManagerService(启动Activity服务等,详细可见奇虎360的插件化方案DroidPlugin)等,还有比如Activity预先占坑,为后面新增的Activity的加入提供支持的。还有各种黑科技,目前已经有许多技术支撑着Android的插件化开发。
不仅如此还有一些热修复/热更新技术:
乐变
微信 Thinker/Andfix/
QQ空间 classloader
热修复,插件化,容器花,组件化开发之间差异分析及应用
热修复中,各个框架有各自框架的优点,主要服务发布到线上之后,产品的bug的修复。
但是如果做到功能热补充,功能热添加,却无能为力。
各个提供的技术层出不穷,如果要说某个技术最为成熟的话,其实各有各的优点。
大量的技术提现在对Android底层的api进行hook,我认为这是一个高风险的做法,主要原因是android平台的机型多,本身就有手机的各种碎片化引起的兼容性问题。
可行性–最佳实践
最终我根据以前架构的经验和总结选择了small作为我们容器化技术,通过路由调用各个组件的能力
架构图
看到这个图是不是很熟悉,我们Android操作系统的也是一个容器,他承载的我们应用的基本的能力和功能,让我们无须关心,屏幕是怎么显示图像的,拨打一个电话是怎么通过芯片拨打出电话,等等等等。
那么在该结构中,带来了什么?
1,首先通过Router 进行组件跳转。
2,代码层级上建立module进行业务区分,解耦,可热插拔,可热更新。
3,组建间代码结构,实现方式互不影响。
4,提供数据提供者供组件间使用。
5,提供对应用在开发期,上线期的性能的侦测和问题的手机。
6,单独的模块可以更快的集成到测试环境中,进行测试。
7,应用在运行期可以动态更新组件,手机日志,修复问题,定位问题。
8,单元测试仅需测试基础功能库的功能,组件内功能可以无需测试。
9,逻辑与界面分开。
10,界面对数据调用通过RxJava实现,无需关心数据存储在哪里。关心数据的显示即可。
11,统一的数据出口。
12,规范化开发方式,提供容易可变的开发。
13,开发人员更加高效的,便携的开发出自己想要的功能,按需使用需要的功能,却不用关心逻辑实现。
开发指南
路由添加
app/src/main/assets/bundle.json
包含了组件的路由映射。具体格式,请参考里面的具体代码。
需要添加新的路由跳转,请在此添加,路由的跳转逻辑,请在app+stub项目中的Router类当中,添加新方法和跳转逻辑
模块约定
以app开头的模块,在app打包的时候,会以apk的形式,打包进项目中,并且apk模块是可以单独运行的模块。
以lib开头的模块,是以功能块的模块,打进主项目,不能被单独调试,他提供了一些功能性的东西,如支付等。按需使用
不以任何开头的模块(简称类库),是以类库的形式(封装好的功能),最终只会以aar/jar的形式打进项目中,需要这功能,依赖他就可以了。
lib开头的模块,和类库,从构建的角度上来说,性质上是一样的,都是提供一些功能。lib模块是可以热更新的,而类库,是不可以热更新的。
app+stub模块是一些公共的资源,一些公共的资源可以放在这里面,但是由于65535限制和设计角度来说,app+stub是不可以集成太多的功能性的东西,是一些基本的功能,他不需要被依赖,开发的时候,他会自动的向lib.,app.(除类库)等,提供功能,须知。
模块说明
app+stub
该包是最基本的项目,里面包含了常用的类库,还有我们八号门的通用数据方式们都在这里面。
包结构:
lib.pays
该项目主要是支付的实现,目前有微信支付和支付宝支付。
sharesdk
此模块是提供分享的功能,当需要向外分享时,依赖此库。
团队约定
为了防止冲突,请尽量按照以下格式
新建的app.xxx module如果是包含其他模块公用的实体,代码,逻辑。放置到app+stub中,放置到对应的模块中
资源,类,遵循能放到模块中,决不放到公共中的原则。 但是该资源如果被多个模块中引用到,请放在app+stub中。
新建module的时候,就是如果是application的项目,需要前缀app.[名字] 如果是库的形式存在则需要 lib.[库名],且每个module的工程的包名,不能相同
比如,有一个模块为user,那么他有个输入框的原本名称是 et_username,那么为了防止跟其他模块冲突,请该id名字,取名为et_user_username
中间的user为模块标识,其他的图片,资源文件,烦请根据这个规则放置.
原本项目中,不要使用butterknife,dagger等需要用apt插件在编译期产生额外代码的插件(尤其在app.的子模块中),请在插件中搜索findViewByMe 等插件,生成findViewById代码。
新建项目可以拷贝app.user这个module,取名一个可以区分的名字,如果是作为library使用,请用lib.xxx[xxx是模块名词]开头,如果是作为App单独运行,请以app..xxx[xxx是模块名词]开头
app+stub是一个公共代码函数区域。目前因为有65535的方法的限制,app+stub的代码要尽量少[+stub是一个分身的意思],如果碰到
方法超过65535的问题,需要拆分+stub中的代码容量,如不了解,请联系我。
Intent传值的时候对应的Key请放在app+stub的net.sinedu.company.config.IntentKey中,读取传值的时候,也请从这边读取全局的公共变量读取相应值,并且注释上该变量的作用。方便维护和他人读取。并且表明传过去的对象类型。
初期,请大家使用intent.getString intent.getInt等方式读取对应的值,后续,我将开发一个功能,方便大家读取值,免去getInt,getString的操作。
EventBus的事件的对象,请放置在net.sinedu.company.event中,方便全局通知值和维护。
子模块和主工程的签名都使用根目录下的sign文件夹下的,帐号密码啥的拷贝其他模块的如app。
可以对模块中的某个activity设置为启动器,在打包进app中,不受影响。
如需读取用户的信息。可以从UserService当中直接读取到(暂未开发)。
缺陷:
不能开启加固和混淆,如果开启,则编译会出现问题
部分需要使用APT在编译器进行编译的插件不支持,例如ARouter,ButterKnife 8.+,DataBind的不支持
通过 Robolectric 库可以让你在不使用真实设备或者模拟器的情况下直接在 Java 虚拟机上进行 JUnit 测试。但目前 Roboletrics 不支持Realm的测试,因为 Realm 包含使用 C++ 的原生库,所以你目前不可以通过 Roboletrics 测试使用 Realm 的功能。
需要先执行gradlew buildLib(打包库),gradlew buildBundle(打包模块)才能主工程才会更新到最新的组件和类库
65535限制,如果调试单个模块,如用户,整个功能库超过了65535个方法是编译不过去的,是可以开启MultiDex功能(在build.gradle当中可见),但是缺陷是打包进app中需要关闭multiDex的功能。解决办法是减少生成数的办法是最好的方式。