SickWorm的博客

Dynamic Feature 上线 1 年实践分享

Android, Android 编译  ·  

目前团队产品 已上线 Dynamic Feature 模块 5 个累计减包约 35MB(64 位包)。上线后无聚集性用户反馈。

1. Dynamic Feature 简要介绍

1.1 Dynamic Feature 是什么

Dynamic Feature,官方全称为 Dynamic Feature Modules,是基于 Multiple APK + Google Play services 的官方动态下发方案。

基于 Multiple APK 指的是加载方式——Dynamic Feature Module 最终会被打包成一个或多个的独立的 apk。手机在安装完 base.apk(除 Dynamic Feature 模块之外打包而成的 apk)后,可以在未来任意时刻,安装其余的 Dynamic Feature APK。

这些 APK 看起来长这样:

apks

那 Dynamic Feature APK 是怎么生成的呢?是 Google Play 用我们在上架时提供的 AAB 包和提前上传的签名文件生成的。生成工具我们也可以拿到,就是 bundle-tool

AAB 格式介绍:Android App Bundle 简介

Multiple APK 介绍:多 APK 支持

基于 Google Play services 指的是加载通道——Dynamic Feature 的下载和安装依赖 Google Play services。加载请求实际上最后调用的是 Google Play 商店 App 进行下载和安装。

1.2 Dynamic Feature 的适用场景

Dynamic Feature 唯一目的是减包。Dynamic Feature 发布和版本上架发布一样,同样需要遵循 Google Play 上架流程,且每个 Dynamic Feature 实现都对应唯一的上架包,因此 Dynamic Feature 无法用于热修复场景

Dyanmic Feature 适用于使用比例较小,但占包体积大非核心功能。如产品上架的一款小游戏,大小约 5MB,且依赖 so 库,而且是非核心使用场景,就非常适合动态下发。

因为 Google Play 不允许下发可执行二进制文件,所以 Dynamic Feature 是出海 App 减包代码资源的唯一合规方式。

减小 APK 体积可以提高下载转化率,降低市场推广成本。

2. 接入阶段

2.1 Dynamic Feature 接入

Dynamic Feature Module 和其他普通 module 一样,是以 gradle module 的形式存在的。如果你需要将一个普通 module 改造为 Dynamic Feature Module,那他的接入步骤大致是:

2.1.1 声明

  1. Module build.gradle 改动:从 com.android.library 改为 com.android.dynamic-feature,表明其构建使用的是 Dynamic Feature 构建而非普通 library 构建。

  2. Module AndroidManifest.xml 添加声明:

    <dist:module
        dist:instant="false"
        dist:title="@string/feature_name">
        <dist:delivery>
    		<dist:on-demand/>
        </dist:delivery>
        <dist:fusing dist:include="true" ></dist:fusing>
    </dist:module>
    

    上面的代码声明了 Dynamic Feature 的名字和安装模式。虽然模块被声明为 Dynamic Feature Module,但还是可以通过配置让其随基础包一同下发。详细参数参考:Overview of Play Feature Delivery

  3. App build.gradle 添加声明:

    android {
    	...
    	dynamicFeatures = [':dynamic-feature-A'] // gradle 模块名
    }
    

2.1.2 依赖改造

  1. 移除其他模块对 Dynamic Feature Module 的依赖。

    理由:Dynamic Feature Module 被调用时不一定已经加载,所以不能被直接依赖。

    官方依赖改造方案:声明一个接口类 ->Dynamic Feature Module 实现接口 -> 运行时调用方通过反射获取实现。

  2. Dynamic Feature Module 依赖 app 模块。

    Dynamic Feature Module 可以自由依赖其他模块,包括 library, app, dynamic feature 模块。构建系统会智能识别并进行打包。详见 2.2.3。

  3. 如果你的工程没有声明 android.nonTransitiveRClass=true,即没有关闭 transtive R,则有额外工作量:将引用了其他 module 资源文件的 R 引用,改为对应模块的 R 类,或统一改为 app 的 R 类。

    transitive R 使得自己模块的 R 类也会包含依赖模块的资源 ID,但声明为 Dynamic Feature Module 后不会再执行 transitive R,导致模块的 R 类无法索引其他模块的资源 ID。

新模块接入会简单一些,因为不需要改造依赖。

2.1.3 运行时加载

  1. 依赖 com.google.android.play:core 库。

  2. Application.attachBaseContext 中增加:SplitCompat.install(this)

  3. 为 Dynamic Feature Module 的所有 Activiy.attachBaseContext 增加:SplitCompat.installActivity(this)

    这个操作会将 base 的 context 附加到 Dynamic Feature Activity 的 context,使其可以正常加载 base 资源。

  4. 通过 SplitInstallManager 加载 Dynamic Feature Module。SplitInstallManager功能包含:

    • 查询一个模块是否已安装;
    • 请求安装模块,并通过注册回调监听下载事件;
    • 请求异步安装(deferred_install),Google Play 会尝试帮你后台下载,即使你的 App 不在前台;
    • 卸载模块。
  5. 使用 SplitInstallManager 请求安装成功后:

    • 代码调用:通过反射获取具体实现
    • 资源引用:资源引用所使用的 context,需要通过 context.createPackageContext(context, 0)创建。根据实践发现,如果 Dynamic Feature 是在此次运行期间安装完成的则必须调用,非本次运行时安装则非必须。

完整接入步骤可以参考官方教程:On Demand Modules

demo 下载:选择 最后一页 里的 “The final code on Github”。

2.1.4 测试

测试 Dynamic Feature 加载一共有三种方式,运行调试,本地测试,在线测试(走 Google Play 商店)。

  1. 运行调试——即配置完成后直接通过 Android Studio 运行。

    Android Studio 支持选择哪些 Dynamic Feature 立即安装。未立即安装的 Dynamic Feature,可以通过 adb 进行后续安装。见:测试 Android App Bundle

    run_configuration

  2. 本地测试——Google 提供了FakeSplitInstallManagerFactory API,通过传入本地的 Dynamic Feature APK 路径来模拟安装流程。APK 生成使用 bundle-tool

  3. 在线测试有两种方式:内部应用分享和内部测试。

两种在线测试都需要 Google Play 管理者权限。

通过内部应用分享上传的 AAB,会被 Google Play 用平台上的一个 debug 签名文件重签名。如果你的 debug 签名文件和平台上的 debug 签名文件不是同一个,则 App 验签相关的逻辑会失败,如微信登录/QQ登录这种会验签的步骤。

额外:是否必须进行在线测试?

就目前实践情况来看,如果你的本地测试和在线测试的加载流程是完全一致的,仅最后的安装使用了对应不同的 API,那么不会出现本地测试通过,但在线测试不通过的情况。

出于时间成本考虑,App 的 Google Play 上架流程中并没有包含在线测试步骤,而是使用了FakeSplitInstallManagerFactory 模拟验证。

2.2 接入可能遇到的问题

2.2.1 报错:The following feature module names contain invalid characters. Feature module names can only contain letters, digits and underscores.

模块名只允许包含字母,数字和下划线,不支持中划线,括号等其他特殊字符。

特别值得一提的是中划线-,Feature Apk 的配置是通过中划线来分割模块名称和配置信息的:

module_name

2.2.2 报错:找不到符号 R.drawable.xxxx / error: resource drawable/ (aka xxx) not found

Dynamic Feature 构建关闭了 transive R 导致。解决方案除了手动改代码,还可以通过插桩解决。这里提供两个插桩参考方案:

  1. 重写 R.jar——processReleaseResource 执行后会生成 R.jar,此时修改字节码改为 Dynamic Feature Module 的 R 类继承 app R 类。这也是产品目前使用的方案。

  2. 插桩调用代码——把 Dynamic Feature 中对 R 类的引用,改为 app R 类的调用。方案来源:58同城无侵入改造业务库为Dynamic Feature工程的探索和实践

2.2.3 报错:different content

原因是 base 和 Dynamic Feature Module 不能有同名资源。需要手动处理。

2.2.4 疑问:Dynamic Feature Module 如果引用了某个 library 模块,这个模块会被打包进 base APK 还是 Dynamic Feature APK?

根据如下规则进行打包,优先级从上往下:

  1. library 被非 Dynamic Feature Module 依赖——打包进 base APK
  2. library 被多个 Dynamic Feature Module 依赖——打包进 base APK
  3. library 仅被一个 Dynamic Feature Module 依赖——打包进 Dynamic Feature APK

2.2.5 疑问:工程改造 Dynamic Feature 后增量构建是否会变快?

不一定。至少在我们工程测试中增量编译时耗时反而变长了。

我尝试性地将录歌模块改造为 Dynamic Feature,通过只增加一行空行来比较编译耗时。编译耗时反而从 30s 增加到了 50s,主要的额外耗时出现在 generateRFile,耗时达到 20-30s。

这个情况与网上以及官方的结论并不一致,网上普遍宣称编译速度可以提高 50%。但这不排除可能是工程的特殊性,如底层资源过多,或一些特殊的 gradle 配置,导致耗时反而增加。

2.2.6 疑问:Dynamic Feature 的更新逻辑是怎样的?

经过验证,发现:

  1. 在 Google Play 升级新版本时,会同时增量更新已安装的 Dynamic Feature Module,更新后启动返回状态是已安装,无需再次请求下载。

  2. 清除 App 数据,不会卸载已安装的 Dynamic Feature Module。(若使用FakeSplitInstallManagerFactory安装则会随着清除 App 数据而卸载)

  3. Dynamic Feature 下载时,可能有 3 种表现:

    • 静默加载;
    • 通知栏显示“正在下载”;
    • 弹窗提示是否下载,用户可选择拒绝。

    出现哪种表现与包体积大小有关,具体逻辑由 Google Play 控制,具体条件为黑盒。但根据上报发现,弹窗占比极低(≈0.12%)

2.2.7 疑问:是否有无法 Dynamic Feature 的功能?

如果你的 Module 依赖了第三方 SDK,而这个第三方 SDK 有自己的 Activity,或者会调用 Res 和 Assets 资源,则这个 Module 无法支持 Dynamic Feature,需要 SDK 适配。

产品的部分广告源 SDK,如字节的 Pangle SDK,已明确不支持 Dynamic Feature。

2.3 我们在 Dynamic Feature 接入上做了一些什么事情

  1. 写了一个 gradle 插件,实现了一键切换普通 library 和 dynamic feature。平时本地开发还是和普通 apk 一样,蓝盾构建才会构建 aab。

    实现一键切换主要出于两个考虑:

    1. 减少对工程的侵入改造。(改造成本1. 修改资源 ID 引用,2. 添加声明代码)

    2. 保持现有的开发模式,保证稳定性。如部分 Oppo / Vivo 手机,不支持安装多 APK,会安装失败。

  2. 蓝盾构建不再使用 assembleRelease 打包 release.apk,而是通过 aab 转 apk,保证 release.apk 和 release.aab 同源,也可减少一次 release 构建。

    但如果要生成仅 32 位和仅 64 位的 apk,bundle-tool 目前并不支持。我简单魔改了一下 bundle-tool,增加了--target-abi 参数,使其支持输出指定架构。

# # # #