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 安装与下载流程

当用户点击某个动态功能时,流程大致如下:

  1. App 调用 SplitInstallManager.startInstall() 请求下载
  2. Play Core 检查 Split APK 是否已在设备上
  3. 如果不在,向 Google Play 发起异步下载请求
  4. 下载完成后,Play 服务自动安装 Split APK(不重启 App)
  5. 安装成功后回调 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)。

下面这张图能帮你快速理解整个动态交付的生命周期:

动态交付(On-Demand Delivery)流程图 App Bundle 发布 Google Play 拆分 Split APK 存储 用户设备(基础 APK) 用户触发功能 下载 Split APK 安装 & 加载 可用(可卸载) 遇到错误:重试或降级

流程其实很清晰:发布 → 拆分 → 用户触发 → 下载 → 安装 → 可用。你想想看,整个过程不需要用户额外操作,体验跟普通功能没区别。但底层涉及 Google Play 服务、Split APK 签名校验、安装验证等复杂逻辑。

关键总结:动态交付的核心理念是“减少首包大小,按需获取功能”。实现上依赖 Play Core Library 和 Split APK 机制。开发者需要处理好加载时序和资源冲突。

嗯,这一部分内容比较多。如果你在项目中遇到动态模块安装后功能不可用,第一步先检查 updateAppInfo() 是否调用;第二步确认 Split APK 是否真的被 Google Play 正确分发(可以通过 Play Console 的“测试工具”验证);第三步检查模块的 manifest 是否有 dist:delivery="on-demand" 声明。