Android组件化开发,Small应用实践

插件化与组件化

插件化就是将一个app分为一个宿主和多个模块(插件),宿主是被真正安装到设备的apk,负责加载插件,每个插件都是一个独立的apk,最终打包发布时宿主和插件分开或者联合打包。

组件化也是将一个app分为一个宿主和多个模块(组件),每个组件可以是一个单独的模块,也可以相互依赖,最终打包发布时宿主和组件打包成一个apk。

Android组件化开发,Small应用实践_第1张图片
组件化与插件化

关于插件化与组件化的解释,这里参考了这篇文章。

为什么组件化

  • 模块解耦,业务模块组件更加独立。
  • 重用公共库模块,减少重复开发和维护的工作量。
  • 并行开发,模块组件支持热更新,加快版本迭代速度,解决用户需要频繁更新app问题。
  • 有效减少编译时间,可以单独编译和调试单个模块,提高开发效率。
  • 方便测试,可以针对单个模块进行测试。

注意:组件化/插件化只是针对一些重运营和大型app的需要而诞生的,如果你的app没有这方面的需求就没必要了,不然反而变得麻烦。

选哪个框架

框架 作者 描述
DroidPlugin 360 插件化框架,免安装运行apk
VirtualApp asLody 插件化框架,与DroidPlugin类似
Small 林光亮 一个轻量,跨平台,高度透明的组件化框架
Atlas 阿里巴巴 手机淘宝的容器化框架,目前了解不多,不评论
DynamicAPK 携程 组件化框架,目前已停止维护
Dynamic-load-apk 百度 组件化框架,使用代理的方式实现Activity生命周期,代码中需要用that代替this

这里只用过DroidPlugin和Small,而最终选择了Small,不用DroidPlugin的主要原因是它不支持插件间的代码和资源相互调用,和项目需求不符合。选择Small主要有以下原因:

  • 已经过商业应用的验证,目前本人已知使用Small的应用有酷狗和千米电商云
Android组件化开发,Small应用实践_第2张图片
酷狗
  • 对项目代码改动不大
  • 支持组件模块间的依赖
  • 文档比较完善

关于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.homeapp.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.Aapp.B 通过依赖 lib.* 共享该库。

issues 318

解决集成 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

Android组件化开发,Small应用实践_第3张图片
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 放 appapp+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,因为很多问题都已经有人遇到并解决了。

你可能感兴趣的:(Android组件化开发,Small应用实践)