58同城无侵入改造业务库为Dynamic Feature工程的探索和实践

导读

本文介绍了无侵入改造业务库为Dynamic Feature工程的探索和实践。探索将业务库改造为Dynamic Feature工程主要基于以下几点的考虑:

  • 本地开发编译时间太长,影响开发效率。将业务库改造为Dynamic Feature工程可以极大提升开发效率。
  • 厂商对内置包大小要求不能超过45M,利用Dynamic Feature模块可以动态分发的特性产出厂商内置包以满足厂商对包大小的要求。
  • 利用Dynamic Feature模块可以动态分发的特性产出只包含特定业务的推广包减少包大小提升推广效果。
  • 国内华为应用市场已经支持aab格式发布应用,相信很快将支持Dynamic Feature模块的动态分发,相当于提前适配趋势。

Android App Bundle简介
Google Play应用市场支持针对不同的设备密度、CPU架构、语言配置等上传多个apk,你可以splits配置构建出多个apk,这样当用户从Google Play下载应用时Google Play会根据用户设备匹配相应的apk进行分发。但由于Android设备类型繁多,开发者每次可能需要上传几十个apk,这对于开发者来说很不友好,所以大部分开发者为了简单方便通常只会上传一个apk适配兼容不同的设备类型。
显然这不是Google想要的结果,所以为了解决这个问题,Google推出了一种新的应用市场上传格式(.aab)Android App Bundle。你可以使用Android Studio提供的功能构建出一个aab文件。

这个是aab文件结构图:



图片引用自android官方文档

使用
BundleTool
可以将aab文件拆分为多个apk,BundleTool的产物其实是一个压缩包,解压后可以看到不同设备密度、CPU架构、语言配置的多个apk。

java -jar bundletool-all-1.0.0.jar build-apks --bundle=app.aab --output=my_app.apks
解压后包含两个文件夹,一个是splits,用于针对android 5.0及以上系统Google Play会根据设备所需资源动态分发所需apk:

下图是一个使用BundleTool将aab拆分为多个apk文件的结构示意图。

图片引用自android官方文档

使用下面的bundletool命令可以将拆分的多个apk根据设备所需安装到手机上:

java -jar bundletool-all-1.0.0.jar install-apks --apks=my_app.apks

笔者使用的是华为Mate20X,使用adb命令可以查看应用path目录下的apk:

adb shell pm path com.sample
package:/data/app/com.sample-DbE93F06pzCzfEd2h4TQJQ==/base.apk
package:/data/app/com.sample-DbE93F06pzCzfEd2h4TQJQ==/split_config.xxhdpi.apk
package:/data/app/com.sample-DbE93F06pzCzfEd2h4TQJQ==/split_config.zh.apk
package:/data/app/com.sample-DbE93F06pzCzfEd2h4TQJQ==/split_java.apk
package:/data/app/com.sample-DbE93F06pzCzfEd2h4TQJQ==/split_java.config.xxhdpi.apk

示例工程中包含3个Dynamic Feature工程,在下一节会详细介绍Dynamic Feature。工程名分别是assets、java、native,使用BundleTool安装后只看到了java的feature apk,这是因为另外两个feature工程在清单文件中配置了按需加载(onDemand设置为true):

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.sample.assets">


<dist:module
dist:onDemand="true"
dist:title="@string/module_assets">
<dist:fusing dist:include="false" />
</dist:module>


<application android:hasCode="false"></application>
</manifest>
另外一个是standalones目录,用于针对android 5.0以下系统不支持安装多apk时分发全功能的apk,可以看到目录中的apk也根据适配屏幕的资源产出了多个apk:

在Google Play应用市场上传aab可以很好地解决前面提到的问题,唯一一点让开发者顾虑的是需要将签名文件上传到应用市场。下图是传统apk和aab动态分发的一个效果图:

图片引用自android官方文档


华为应用市场目前已经支持使用app bundle发布应用,当然也需要加入应用签名 计划,就是需要上传签名文件。


详见
https://developer.huawei.com/consumer/cn/doc/distribution/app/18527283


截图自华为应用市场
Dynamic Feature简介
Android App Bundle中可以包含动态交付的功能,在新建Module时你可以选择Dynamic Feature Module,Dynamic Feature的插件是apply plugin: ‘com.android.dynamic-feature’,dynamic-feature插件的产物是一个apk。


既然Dynamic Feature工程的产物是一个apk,那么能不能将业务库改造为Dynamic Feature库?从而满足背景中提到的3点需求:


1.首次全量编译后缓存base apk和feature apk,业务需求开发第二次只编译改动的业务库产出feature apk,配合首次全量编译缓存的apk完成安装,提升开发期间编译速度;
2.快速产出业务推广包;
3.产出厂商内置包满足厂商对包大小的要求。

答案是肯定的,需要考虑的是能否无侵入或低侵入现有工程结构代码,可以开关动态控制,不影响线上包。
Dynamic Feature工程依赖关系

  • feature工程需要依赖app工程,app工程不能依赖feature工程,这时feature工程的产物是feature apk,app工程的产物是base apk。
  • 如果feature A和feature B共同依赖了library A,此时library A需要添加到base的依赖树中,这时所有base依赖的library会产出base apk,feature工程和它的独有依赖会产出feature apk。

Dynamic Feature工程资源合并、访问
在插件化技术中,我们需要修改aapt源码实现修改插件资源id,避免和宿主apk产生资源冲突。在Android App Bundle中,gradle帮我们实现了这个功能,base apk中的资源id是从0x7f开始,其它feature apk资源id依次减1:

资源合并访问:

  • 在构建时会将所有feature工程的清单文件合并到base apk中,所以feature工程清单文件中引用的主题图片等资源需要放到base工程可以依赖的lib中。
    根据上面依赖关系和资源冲突解决的讲解,如果在feature工程访问base工程中的资源,我们需要使用base包名的R文件访问base工程中的资源,反之我们在base工程中需要使用feature包名的R文件访问feature apk中的资源。

无侵入改造业务库为Feature工程方案
下图是app的工程结构图,上层依赖于下层,最上层是应用壳工程产物是app的apk,下层是所有业务库和基础库应用的插件是Library产物是aar,各业务库之间代码不可见。


想要将业务库改造为Feature工程,首先需要将library插件修改为dynamic-feature插件,其次是修改依赖关系,feature工程需要依赖base工程,产生的问题有两个:一个是修改插件后编译时会出现很多资源找不到的问题,原因就是我们前面提到的资源合并规则,因为业务库依赖的公共库被打包到了base中,所以我们需要修改大量的代码将找不到的资源包名修改为base的包名,这对我们来说改造成本太大了,且后续维护成本很高;另一个问题是各业务线之间代码是不可见的,各业务线本地开发时依赖其他业务线的aar代码,aar代码无法作为一个Feature工程。
解决依赖关系和业务线代码不可见问题
为了解决上述问题,我们针对每个业务库新增了一个Feature壳工程,应用dynamic-feature插件,依赖base工程和各业务线的aar库,这个壳工程解决了各业务线代码不可见和依赖关系的问题,并且对现有工程结构无影响。


解决资源访问问题
对于资源访问问题,解决思路如下,feature中的资源访问分两种情况:一是访问的资源本身不在feature apk中,可以通过feature的R资源文件查询,这时我们直接将访问资源的包名修改为base的包名;另一种情况是访问的资源可能在base apk中,也可能在feature apk中,这时我们需要从feature和base中遍历查找到真正的资源。

根据上面的思路我们通过自定义gradle插件使用asm修改字节码,完成无侵入解决资源访问问题。主要流程如下:
1.使用asm扫描class,首先收集Feature的R资源文件信息,如果访问的R资源不在集合中,则直接修改为base包名。
2.处理findViewById(),解决feature库和base库存在相同id,但使用的资源在base中,通过分别查找feature和base中的资源,最后通过反射,最终找到正确的view。
3.处理getResource(),解决使用getResource().getIdentifier获取id值不正确的情况,从feature中获取资源时包名需要使用base包名+featureModuleName。
4.处理onClick方法中使用v.getId()判断view的情况,通过资源名称获取真正的view的id。

对于base访问feature中的资源,通常是base中引用的资源被feature中的子类重写了,这种情况相对较少,所以我们通过添加白名单根据上面的方案只处理指定的类来避免影响构建速度

Feature工程Arouter路由注册问题
项目中部分业务使用了Arouter路由框架,使用apply plugin: ‘com.alibaba.arouter’插件可以完成Arouter的路由注册,该插件的原理是先收集Arouter注解处理器生成的类信息,然后使用ASM修改字节码完成路由表的注册。Arouter路由框架目前还不支持Dynamic Feature模块,所以将该业务改造为Feature工程后导致无法跳转。

解决方案如下:

  • 因为Feature工程是一个壳工程,肯定不会包含Arouter相关代码逻辑,所以不需要使用Arouter的注解处理器生成相关类
  • Feature壳工程依赖的aar中已经包含Arouter注解处理器生成的类文件
  • 参考Arouter插件收集Arouter生成的类文件信息
  • 在Feature Application的onCreate方法中使用ASM参考Arouter插件反射调用Arouter中LogisticsCenter.register方法完成Feature库中Arouter相关注册
  • 最后在base工程中Application初始化时完成Feature Application的初始化即可

成果
至此,我们完成了无侵入改造业务库为Feature库。并且可以通过开关控制开发模式,对线上包无影响。在全量编译时base和feature会并行构建,所以构建速度上会有所提升,在增量编译时只需要编译改动的业务feature库,构建速度大幅提升约60%-70%。
最后,我们自定义gradle task在全量编译完成后将所有apk进行缓存,增量编译时只编译对应业务feature工程产出feature apk配合全量编译产出的其他apk,然后使用adb install-multiple命令完成安装即可。注意:oppo和vivo手机不支持 adb install-multiple命令。

adb install-multiple base.apk feature1.apk feature2.apk

总结
Google Play推出的Android App Bundle解决了开发者为了适配不同类型设备需要上传多个apk的问题,使用BundleTool可以将aab根据设备屏幕密度、CPU架构和语言配置拆分为多个apk,用户从应用市场下载时只会推送用户设备所需资源的apk,从而减少了用户下载apk的大小,提升了下载率和安装成功率。
Android App Bundle中可以包含Dynamic Feature模块,它的产物是一个apk,我们探索出了无侵入将业务库改造为Feature工程的方案,为后续提供厂商基础包和业务推广包打下了基础。得益于gradle的并行构建(base和feature会同时构建),所以全量编译构建速度有所提升,将首次全量编译的apk进行缓存,业务线在增量编译时只构建对应业务feature工程构建速度提升60%-70%,配合全量编译产出的其他apk,然后使用adb install-multiple命令完成安装即可。
后续规划
在无侵入将业务库改造为Dynamic Feature工程之后,如果想实现Feature apk的动态分发,需要借助Google Play Core Library提供的api,由于众所周知的原因,国内是无法使用的。所以后续我们会使用插件化技术实现Feature apk的动态分发并产出厂商内置包和业务推广包。



作者简介:

于卫国,
用户价值增长中心资深开发工程师 

况众文
,用户价值增长中心资深开发工程师

王永川,
用户价值增长中心高级开发工程师

栗庆庆
,用户价值增长中心高级开发工程师



参考文献:

  • https://developer.android.com/guide/app-bundle
  • https://developer.huawei.com/consumer/cn/doc/distribution/app/18527283
  • https://developer.android.google.cn/studio/build/configure-apk-splits.html?hl=en
  • https://zhuanlan.zhihu.com/p/86995941
  • https://github.com/google/bundletool
  • https://github.com/alibaba/ARouter



推荐阅读:




基于Flink构建实时数仓实践




基于一种改进的Wide&Deep 文本分类在用户身份识别上的实践

福利环节

为了鼓励优质内容传播,【58技术】公众号近期会持续推出不定期活动奖励。

  1. 评论区互动留言,即可参与此次活动
  2. 留言转发集赞,点赞量前三名(点赞数需大于10)可获得定制版新年代码台历一本
  3. 活动时间:截至2021年1月10日