目前团队产品 已上线 Dynamic Feature 模块 5 个:累计减包约 35MB(64 位包)。上线后无聚集性用户反馈。
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 看起来长这样:
那 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 进行下载和安装。
Dynamic Feature 唯一目的是减包。Dynamic Feature 发布和版本上架发布一样,同样需要遵循 Google Play 上架流程,且每个 Dynamic Feature 实现都对应唯一的上架包,因此 Dynamic Feature 无法用于热修复场景。
Dyanmic Feature 适用于使用比例较小,但占包体积大非核心功能。如产品上架的一款小游戏,大小约 5MB,且依赖 so 库,而且是非核心使用场景,就非常适合动态下发。
因为 Google Play 不允许下发可执行二进制文件,所以 Dynamic Feature 是出海 App 减包代码资源的唯一合规方式。
减小 APK 体积可以提高下载转化率,降低市场推广成本。
Dynamic Feature Module 和其他普通 module 一样,是以 gradle module 的形式存在的。如果你需要将一个普通 module 改造为 Dynamic Feature Module,那他的接入步骤大致是:
Module build.gradle
改动:从 com.android.library
改为 com.android.dynamic-feature
,表明其构建使用的是 Dynamic Feature 构建而非普通 library 构建。
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
App build.gradle
添加声明:
android {
...
dynamicFeatures = [':dynamic-feature-A'] // gradle 模块名
}
android.nonTransitiveRClass=true
,即没有关闭 transtive R,则有额外工作量:将引用了其他 module 资源文件的 R 引用,改为对应模块的 R 类,或统一改为 app 的 R 类。
transitive R 使得自己模块的 R 类也会包含依赖模块的资源 ID,但声明为 Dynamic Feature Module 后不会再执行 transitive R,导致模块的 R 类无法索引其他模块的资源 ID。
新模块接入会简单一些,因为不需要改造依赖。
com.google.android.play:core
库。
Application.attachBaseContext
中增加:SplitCompat.install(this)
。
Activiy.attachBaseContext
增加:SplitCompat.installActivity(this)
这个操作会将 base 的 context 附加到 Dynamic Feature Activity 的 context,使其可以正常加载 base 资源。
SplitInstallManager
加载 Dynamic Feature Module。SplitInstallManager
功能包含:
SplitInstallManager
请求安装成功后:
context.createPackageContext(context, 0)
创建。根据实践发现,如果 Dynamic Feature 是在此次运行期间安装完成的则必须调用,非本次运行时安装则非必须。完整接入步骤可以参考官方教程:On Demand Modules。 demo 下载:选择 最后一页 里的 “The final code on Github”。
测试 Dynamic Feature 加载一共有三种方式,运行调试,本地测试,在线测试(走 Google Play 商店)。
FakeSplitInstallManagerFactory
API,通过传入本地的 Dynamic Feature APK 路径来模拟安装流程。APK 生成使用 bundle-tool。
两种在线测试都需要 Google Play 管理者权限。
通过内部应用分享上传的 AAB,会被 Google Play 用平台上的一个 debug 签名文件重签名。如果你的 debug 签名文件和平台上的 debug 签名文件不是同一个,则 App 验签相关的逻辑会失败,如微信登录/QQ登录这种会验签的步骤。
额外:是否必须进行在线测试?
就目前实践情况来看,如果你的本地测试和在线测试的加载流程是完全一致的,仅最后的安装使用了对应不同的 API,那么不会出现本地测试通过,但在线测试不通过的情况。
出于时间成本考虑,App 的 Google Play 上架流程中并没有包含在线测试步骤,而是使用了FakeSplitInstallManagerFactory
模拟验证。
The following feature module names contain invalid characters. Feature module names can only contain letters, digits and underscores.
模块名只允许包含字母,数字和下划线,不支持中划线,括号等其他特殊字符。
特别值得一提的是中划线-
,Feature Apk 的配置是通过中划线来分割模块名称和配置信息的:
找不到符号 R.drawable.xxxx
/ error: resource drawable/ (aka xxx) not found
Dynamic Feature 构建关闭了 transive R 导致。解决方案除了手动改代码,还可以通过插桩解决。这里提供两个插桩参考方案:
different content
原因是 base 和 Dynamic Feature Module 不能有同名资源。需要手动处理。
根据如下规则进行打包,优先级从上往下:
不一定。至少在我们工程测试中增量编译时耗时反而变长了。
我尝试性地将录歌模块改造为 Dynamic Feature,通过只增加一行空行来比较编译耗时。编译耗时反而从 30s 增加到了 50s,主要的额外耗时出现在 generateRFile,耗时达到 20-30s。
这个情况与网上以及官方的结论并不一致,网上普遍宣称编译速度可以提高 50%。但这不排除可能是工程的特殊性,如底层资源过多,或一些特殊的 gradle 配置,导致耗时反而增加。
经过验证,发现:
FakeSplitInstallManagerFactory
安装则会随着清除 App 数据而卸载)
出现哪种表现与包体积大小有关,具体逻辑由 Google Play 控制,具体条件为黑盒。但根据上报发现,弹窗占比极低(≈0.12%)。
如果你的 Module 依赖了第三方 SDK,而这个第三方 SDK 有自己的 Activity,或者会调用 Res 和 Assets 资源,则这个 Module 无法支持 Dynamic Feature,需要 SDK 适配。
产品的部分广告源 SDK,如字节的 Pangle SDK,已明确不支持 Dynamic Feature。
--target-abi
参数,使其支持输出指定架构。