美文网首页Android安卓架构Android知识
Android组件化开发,Small应用实践

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

作者: CYRUS_STUDIO | 来源:发表于2017-05-29 01:21 被阅读1725次

    插件化与组件化

    插件化就是将一个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

    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

    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 175issues 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,因为很多问题都已经有人遇到并解决了。

    相关文章

      网友评论

      • 混混_X:博主,问下,您在small里面用过百度地图吗?我的插件里面添加了地图,编译的时候报错,Cause: PARSE ERROR:
        class name (com/baidu/platform/comapi/map/a) does not match path (com/baidu/platform/comapi/map/A.class)
        ...while parsing com/baidu/platform/comapi/map/A.class

        PARSE ERROR:
        class name (com/baidu/platform/comapi/map/a) does not match path (com/baidu/platform/comapi/map/A.class)
        ...while parsing com/baidu/platform/comapi/map/A.class
      • 你好_ddb0:感谢博主,解决了我的打包问题
      • 6e4d0684329b:关于bmob踩过相同的坑
        CYRUS_STUDIO: @FuckingCode 最后怎么解决了?

      本文标题:Android组件化开发,Small应用实践

      本文链接:https://www.haomeiwen.com/subject/qfckfxtx.html