Android组件化开发,Small应用实践
插件化与组件化
插件化就是将一个app分为一个宿主和多个模块(插件),宿主是被真正安装到设备的apk,负责加载插件,每个插件都是一个独立的apk,最终打包发布时宿主和插件分开或者联合打包。
组件化也是将一个app分为一个宿主和多个模块(组件),每个组件可以是一个单独的模块,也可以相互依赖,最终打包发布时宿主和组件打包成一个apk。
组件化与插件化关于插件化与组件化的解释,这里参考了这篇文章。
为什么组件化
- 模块解耦,业务模块组件更加独立。
- 重用公共库模块,减少重复开发和维护的工作量。
- 并行开发,模块组件支持热更新,加快版本迭代速度,解决用户需要频繁更新app问题。
- 有效减少编译时间,可以单独编译和调试单个模块,提高开发效率。
- 方便测试,可以针对单个模块进行测试。
注意:组件化/插件化只是针对一些重运营和大型app的需要而诞生的,如果你的app没有这方面的需求就没必要了,不然反而变得麻烦。
选哪个框架
框架 | 作者 | 描述 |
---|---|---|
DroidPlugin | 360 | 插件化框架,免安装运行apk |
VirtualApp | asLody | 插件化框架,与DroidPlugin类似 |
Small | 林光亮 | 一个轻量,跨平台,高度透明的组件化框架 |
Atlas | 阿里巴巴 | 手机淘宝的容器化框架,目前了解不多,不评论 |
DynamicAPK | 携程 | 组件化框架,目前已停止维护 |
Dynamic-load-apk | 百度 | 组件化框架,使用代理的方式实现Activity生命周期,代码中需要用that代替this |
这里只用过DroidPlugin和Small,而最终选择了Small,不用DroidPlugin的主要原因是它不支持插件间的代码和资源相互调用,和项目需求不符合。选择Small主要有以下原因:
- 已经过商业应用的验证,目前本人已知使用Small的应用有酷狗和千米电商云
- 对项目代码改动不大
- 支持组件模块间的依赖
- 文档比较完善
关于Small与各框架的详细对比可以看这里。
Small
- Small Github:https://github.com/wequick/Small
- Small 文档:http://code.wequick.net/Small/cn/home
- Small FAQ:https://github.com/wequick/Small/wiki/Android-FAQ
1. 集成Small
关于如何集成Small可以查看文档这里
2. 项目结构说明
Small 将一个 APK 拆分为多个公共库插件、业务模块插件,它们都是 Android Studio 下的一个 Module。
- 业务模块插件:
Phone & Tablet Module
,模块名称格式app.*
,包名格式packageName.app.*
。 - 公共库插件:
Android Library
,模块名称格式lib.*
,packageName.lib.*
。
Small 通过特定的包名格式识别插件,所以包名需要符合规范。
这里以 Small 的 sample 项目为例子,对各模块做一个简单说明:
- app:宿主模块,一般只加载和启动插件,不包含业务逻辑
- app+stub:app模块的子模块,该模块的代码和资源为其他模块所共享,打包时将自动并入app模块,用于存放各模块共享的资源和代码。
- app.detail:业务模块插件
- app.home:业务模块插件
- app.main:业务模块插件
- app.mine:业务模块插件
- app.ok-if-stub:访问
app.stub
模块资源测试 - jni_plugin:jni库依赖测试
- lib.analytics:数据统计库
- lib.style:样式库
- lib.utils:工具类库
- web.about:本地web网页模块
关于业务模块插件的划分,在项目中我是以业务模块页面跳转为一个分界点,比如业务流程是: 登录注册 -> 主页 -> 直播室
,那么就划分为 app.user
, app.home
和 app.live
。
3. 依赖关系
- 宿主不能依赖任何插件
-
lib.*
之间不能相互依赖(代码可以,资源不可以,建议还是不要依赖) -
app.*
可以依赖lib.*
4. 打包发布
关于插件的编译打包流程可以查看Small的文档,下面是我在编译打包过程遇到的一些问题:
- 如果
lib.*
中的资源有增减,先把public.txt
删除,build 时会自动重新生成资源id,否则有可能遇到Resources$NotFoundException
。 - 正式打包发布时建议完整执行一遍
cleanLib -> cleanBundle -> buildLib -> buildBundle
命令,并确保每个步骤顺利编译。 - 插件编译打包完成后就在
app\smallLibs
目录下,现在 app(宿主) 模块就是一个完整的项目了,对 app 模块打包签名就可以了。
5. 实际应用中遇到的问题
统一管理不同 Module 的依赖库版本
如果不同的插件中引用了同一个第三方库的不同版本,可能出现 pre-verified 异常。
所以,注意抽取公共库和 统一管理不同 Module 的依赖库版本。
配置插件(so)生成目录
Small 默认情况下是把插件生成到 armeabi 目录下,如果想更改可以在 local.properties
添加如下配置:
bundle.arch=armeabi-v7a
或者在 project-level 下的 build.gradle 中添加如下配置(建议):
System.setProperty("bundle.arch", "armeabi-v7a")
或者以命令行参数方式设置
gradlew buildLib -Dbundle.arch=armeabi-v7a
small plugin 中通过读取
bundle.arch
属性设置插件输出目录,具体可以查看RootExtension.getBundleOutput
app.A和app.B都依赖同样的第三方库(jar,aar)会不会冲突?
会的,公共库可以放在 app.stub
或者 lib.*
中,app.A
和 app.B
通过依赖 lib.*
共享该库。
解决集成 Bmob 时 okhttp 库冲突问题
这是原来的配置:
compile "cn.bmob.android:bmob-sdk:3.5.0"
错误日志如下:
Error:Execution failed for task ':demo:transformClassesWithJarMergingForDebug'.
> com.android.build.api.transform.TransformException: java.util.zip.ZipException: duplicate entry: okhttp3/Address.class
尝试过下面的方案:
compile ("cn.bmob.android:bmob-sdk:3.5.0") {
exclude group: "com.squareup.okhttp3"
exclude group: "com.squareup.okio"
}
理论上这样该结束了,但遇到的情况还要复杂一点,有 lib.a 和 lib.b,lib.b 依赖 lib.a(bmob-sdk在这里),app 依赖 lib.b,我在 lib.a 中添加如上配置发现并没有效果,用 everything 搜了一下,发现 lib.b 的 build 目录下也有一个 bmob-sdk
bmob-sdk多个 Module 包含重复的库可以在 app 目录下的 build.gradle 添加如下配置过滤掉重复的库
android {
configurations {
all*.exclude group: "com.squareup.okio", module: "okio"
all*.exclude group: "com.squareup.okhttp3"
all*.exclude group: 'com.google.code.gson'
}
}
解决方法出自这里
编译是没问题了,但是后来打包插件 so 时发现 bmob-sdk 中的 okhttp, okio, gson 还是会被打进去,会导致启动失败...
最终的解决方案是把 bmob-sdk-3.5.0.aar(具体位置可以用 everything 搜索一下) 中的 res, jni 和 libs 中的 BmobSDK_3.5.0_20160630.jar 直接拷贝的自己的 lib 工程对应目录,然后去掉 gradle 中的依赖配置,这样就不存在冲突了。
AndroidManifest.xml
注意把第三方库或者SDK需要用到的权限和相关组件的配置添加到 app(宿主)或者 app+stub 模块下的 AndroidManifest.xml。
In strict mode, we do not allow vendor aars
Execution failed for task ':app.test:processReleaseResources'.
> In strict mode, we do not allow vendor aars, please declare them in host build
- compile('com.android.support:recyclerview-v7:25.0.0')
- compile('com.android.support:design:25.0.0')
or turn off the strict mode in root build.gradle:
small {
strictSplitResources = false
}
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug
解决办法:
- cleanLib,再buildLib试下
- 添加
strictSplitResources = false
配置
参考 issues 175 和 issues 201
怎样判断是否 Debug 模式
开始时想通过访问 lib 中的 BuildConfig.DEBUG
在各插件中判断是否 debug 模式,后来发现 lib 中的 BuildConfig.DEBUG
只会一直返回 false。最后是通过访问 application 节点的 android:debuggable
解决了该问题。解决方案出自这里。
/**
* app 是否 debug 模式
*
* @param context
*/
public static boolean isDebug(Context context) {
if (isDebug == null) {
isDebug = context.getApplicationInfo() != null &&
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}
return isDebug;
}
自定义 Application 放哪里
Small 支持每个插件有自己的 Application(支持 MultiDexApplication),在插件被加载时执行 Applicaiton 的生命周期方法,但一般情况下我们只需要一个 Application。
如果把自定义 Application 放 app
或 app+stub
,无法访问 lib 模块下的代码,放在某个插件下其他插件又访问不了,所以放在一个 lib 下最合适,所有插件通过引用该 lib 并在 AndroidManifest.xml
的 application 节点配置自定义 Application。
由于插件被加载时都会执行一次 Application 的生命周期,所以为了防止重复初始化,这里通过一个静态的布尔值变量 isInited
记录是否已经初始化。示例代码如下:
public class MyApplication extends Application {
private static boolean isInited = false;
@Override public void onCreate() {
super.onCreate();
if (!isInited) {
isInited = true;
init();
}
}
private void init(){
}
}
这样,无论是整包运行,还是调试单个插件都能正常完成 Application 的初始化。
更多问题建议查看 Small 的 issues,因为很多问题都已经有人遇到并解决了。