4. 动态交付(On-Demand Delivery)原理
动态交付,说白了就是“按需下载”。同一款 App 里的某些功能,用户不点就不下载,点了才从 Google Play 拉下来。这样一来,安装包体积能大幅缩减,用户首装体验也好很多。
我第一次接触这个机制是在做一个电商 App 改造的时候。当时安装包已经飙到 80MB,用户抱怨连篇。我们决定把直播购物、AR 试妆这些低频功能做成动态模块……效果确实不错,首包降到了 35MB。
4.1 什么是动态交付
Google Play 在 App Bundle 体系里定义了三类交付模式:安装时交付(Install-Time)、条件交付(Conditional)、按需交付(On-Demand)。动态交付指的就是第三种——用户主动触发后才下载。
| 交付模式 | 下载时机 | 典型场景 |
|---|---|---|
| Install-Time | 用户从 Play 商店安装时 | 基础功能、核心 UI |
| Conditional | 安装时 + 满足条件时 | 根据语言、国家、API 级别下发 |
| On-Demand | 运行时,用户触发后 | 直播、AR、客服、支付扩展 |
注意:条件交付虽然也能缩减包体,但它依然是“安装时决策”。动态交付才是真正的“运行时按需”。
4.2 核心机制:Split APK 与 Play Core
Google Play 会把动态模块打包成独立的 Split APK。这些 APK 不包含在基础 APK 里,而是单独签名、单独分发。
运行时我们通过 Play Core Library 来触发下载。嗯,这里要注意:Play Core 内部用的是 Google Play 服务的异步下载通道,跟普通的 DownloadManager 不同。
// 在 build.gradle 中声明动态模块
android {
dynamicFeatures = [":feature_live", ":feature_ar"]
}
// 每个动态模块的 build.gradle
plugins {
id 'com.android.dynamic-feature'
}
我个人习惯把动态模块的包名保持跟应用 ID 一致,只通过 module name 区分。这样后期混淆和资源引用都不容易出错。
4.3 安装与下载流程
当用户点击某个动态功能时,流程大致如下:
- App 调用
SplitInstallManager.startInstall()请求下载 - Play Core 检查 Split APK 是否已在设备上
- 如果不在,向 Google Play 发起异步下载请求
- 下载完成后,Play 服务自动安装 Split APK(不重启 App)
- 安装成功后回调
onSuccess(),App 加载对应代码和资源
听起来简单?我曾经踩过一个坑——第一次测试时发现下载成功了但资源加载报错。后来排查是因为我在回调里直接用了 Context.createPackageContext(),但没正确处理 ClassLoader。正确的做法是:
我的经验:安装成功后,使用 SplitInstallHelper.updateAppInfo() 刷新应用信息,再通过 PackageManager.getApplicationInfo() 拿到新模块的 Resource 和 ClassLoader。
// 正确的加载方式
SplitInstallManager manager = SplitInstallManagerFactory.create(context);
manager.startInstall(request)
.addOnSuccessListener(sessionId -> {
SplitInstallHelper.updateAppInfo(context); // 关键步骤
// 然后加载模块中的 Activity
Intent intent = new Intent()
.setClassName(context, "com.example.feature_live.LiveActivity");
startActivity(intent);
});
重要:动态模块中的资源必须在 updateAppInfo() 之后才能通过 R 文件访问。如果你发现资源 ID 为 0 或者崩溃,十有八九是忘了这一步。
4.4 资源管理与卸载策略
动态模块下载后,会占用设备存储。如果你不再需要某个功能,可以主动卸载。不过 Google Play 不会自动清理,需要 App 自己触发。
- 卸载接口:
SplitInstallManager.deferredUninstall(),它只是标记“推迟卸载”,等用户下次重启 App 时才会真正清理。 - 立即卸载:Google 不提供强制立即卸载的 API,因为可能导致运行中的进程出问题。我通常的做法是在设置页面提供一个“清理不常用模块”的按钮,调用
deferredUninstall()后提示用户重启 App。
另外,资源冲突也是容易出问题的地方。动态模块的 R 资源 ID 跟基础模块是独立的,但如果你在动态模块里引用了基础模块的 layout 或者 drawable,编译时没问题,运行时如果基础模块资源被混淆或 ProGuard 移除……嗯,就等着崩溃吧。我的方案是:动态模块只依赖基础模块的 res/ 资源,不要依赖基础模块的代码资源(比如 R.anim 之类的内部 ID)。
下面这张图能帮你快速理解整个动态交付的生命周期:
流程其实很清晰:发布 → 拆分 → 用户触发 → 下载 → 安装 → 可用。你想想看,整个过程不需要用户额外操作,体验跟普通功能没区别。但底层涉及 Google Play 服务、Split APK 签名校验、安装验证等复杂逻辑。
关键总结:动态交付的核心理念是“减少首包大小,按需获取功能”。实现上依赖 Play Core Library 和 Split APK 机制。开发者需要处理好加载时序和资源冲突。
嗯,这一部分内容比较多。如果你在项目中遇到动态模块安装后功能不可用,第一步先检查 updateAppInfo() 是否调用;第二步确认 Split APK 是否真的被 Google Play 正确分发(可以通过 Play Console 的“测试工具”验证);第三步检查模块的 manifest 是否有 dist:delivery="on-demand" 声明。