应用功能越做越多,安装包体积也随之水涨船高,但用户真正常用的可能只是其中20%的功能。Feature Delivery正是为解决这个矛盾而生:它是Google在Android App Bundle(AAB)发布机制之上提供的能力,允许开发者将应用拆分为一个基础模块和多个动态功能模块,用户安装应用时只需要下载基础部分,其余功能在需要时按需获取。本文将系统讲解Feature Delivery的工作机制、三种分发模式以及完整的代码接入实践。

Feature Delivery的底层机制:AAB与动态模块的关系
理解Feature Delivery的第一步,是弄清楚Android App Bundle与传统APK的区别。传统方式下,开发者直接上传APK,所有用户的设备下载的都是同一个包,里面包含了全部CPU架构的so库、全部密度的资源和全部语言资源。而AAB是一种发布格式,Google Play接收AAB后,会针对每台设备的特征(屏幕密度、CPU架构、语言)生成裁剪过的split APK再下发,仅这一项通常就能减少15%到25%的体积。
Feature Delivery在这个基础上更进一步。工程中除主模块外,每个用Gradle插件声明的动态功能模块(dynamic feature)都会被单独拆分。构建产物大致分为三类:base APK,包含应用启动必需的代码和资源;feature APK,即各个动态功能模块的代码资源包;configuration APK,存放与设备特征相关的裁剪资源。安装应用时Google Play只下发base APK和对应的configuration APK,动态功能模块真正被请求时才下发feature APK。
需要特别注意的是,动态功能模块的代码在编译期对base模块是可见的,反过来base模块访问动态模块的类则需要通过反射或依赖倒置,这是很多接入者第一次踩坑的地方。模块之间的访问关系在AndroidManifest.xml中通过dist:fusing和运行时的SplitCompat机制共同约束,后面代码部分会详细展开。
三种分发模式:按需、条件与即时分发如何选择
Feature Delivery提供了三种模块分发方式,理解它们的差异是架构设计的关键。第一种是按需分发(on-demand),模块默认不安装,应用运行时通过Play Core Library发起安装请求,用户确认后Google Play后台下载并安装。这适合相机滤镜、地图离线包、高级编辑工具这类低频功能,能显著压缩首次安装体积。
第二种是条件分发(conditional delivery),开发者在模块的manifest中声明安装条件,比如设备是否支持VR、是否在中国区、Android版本是否高于某个API Level。条件在设备侧评估,满足条件的设备在初次安装应用时就自动带上该模块,不满足的设备则永远不会下载。它与按需分发的区别在于:条件分发的决策权在开发者手里,用户无感知;按需分发的决策权在用户使用过程中。
第三种是即时分发(instant delivery),模块被标记为instant-enabled后可以配合Google Play Instant,让用户免安装直接体验。声明方式很简单,在模块的build.gradle中配置:
// 动态功能模块的build.gradle
plugins {
id 'com.android.dynamic-feature'
}
android {
dynamicFeatures = [":dynamic_feature_camera"]
}
// 在模块manifest中声明按需分发
<dist:module
dist:instant="false"
dist:title="@string/title_camera">
<dist:fusing dist:include="true" />
</dist:module>
实际选型时可以组合使用:核心流程放base,低频重资源功能用按需分发,设备强相关能力用条件分发,营销试玩场景接入即时分发。一个典型配置是相机模块按需下载、AR模块条件分发(仅支持ARCore的设备安装),这样入门机型用户的基础包可能只有十几MB。
代码实战:用Play Core Library实现模块的安装与状态监听
运行时安装动态模块依赖Play Core Library。首先在base模块中添加依赖:implementation 'com.google.android.play:feature-delivery-ktx:2.1.0'。核心入口是SplitInstallManager,通过它发起安装请求、监听状态、查询已安装模块。下面是一段完整的Kotlin实现:
private lateinit var manager: SplitInstallManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
manager = SplitInstallManagerFactory.create(this)
requestCameraFeature()
}
private fun requestCameraFeature() {
val request = SplitInstallRequest.newBuilder()
.addModule("dynamic_feature_camera")
.build()
manager.startInstall(request)
.addOnSuccessListener { sessionId -> 保存sessionId用于后续跟踪 }
.addOnFailureListener { e ->
if (e is SplitInstallException && e.errorCode
== SplitInstallErrorCode.NETWORK_ERROR) {
showToast("网络异常,请稍后重试")
}
}
}
// 注册状态监听器,跟踪下载与安装进度
private val listener = SplitInstallStateUpdatedListener { state ->
when (state.status()) {
SplitInstallSessionStatus.DOWNLOADING ->
updateProgress(state.bytesDownloaded(), state.totalBytesToDownload())
SplitInstallSessionStatus.INSTALLED -> {
// 模块安装完成,加载新功能
if (state.moduleNames().contains("dynamic_feature_camera")) {
launchCameraActivity()
}
}
SplitInstallSessionStatus.FAILED ->
Log.e("SplitInstall", "安装失败: ${state.errorCode()}")
else -> Unit
}
}
override fun onResume() {
super.onResume()
manager.registerListener(listener)
}
override fun onPause() {
super.onPause()
manager.unregisterListener(listener)
}
这里有几个容易出错的细节。第一,SplitInstallStateUpdatedListener必须在生命周期中注册与反注册,否则会造成内存泄漏。第二,动态模块安装完成后,如果模块内含Activity,需要确认SplitCompat.installActivity已被调用(在KTX库中Activity会自动处理),否则模块内的资源会找不到。第三,安装完成的模块如果依赖新的代码入口,启动Activity时要用完整包名,因为编译期base模块并没有直接引用它的类。
对于希望用户无感知下载的场景,可以使用延迟安装(deferred install),调用manager.deferredInstall("module_name")后,Google Play会在设备空闲、连接Wi-Fi等合适时机静默下载,不会弹出确认对话框。相应地,模块不再需要时调用deferredUninstall可以回收空间。延迟安装没有session状态回调,只能通过deferredInstallSessionState查询结果,适合预加载一些用户大概率会用的轻量模块。
接入过程中的常见坑与排查思路
第一个高频问题是本地调试。Feature Delivery依赖Google Play的服务端拆分,本地无法直接复现,官方提供了bundletool来模拟:使用bundletool build-apks --mode=universal生成包含全部模块的APK集合,配合bundletool install-apks推送到设备,即可验证模块逻辑。要注意universal模式下所有模块都已安装,无法真实模拟按需下载流程,只能验证代码正确性。
第二个坑是模块间资源与类访问。动态模块可以访问base的代码,base访问动态模块必须通过反射,例如:
val activityClass = Class.forName(
"com.example.feature.camera.CameraActivity")
startActivity(Intent(this, activityClass))
更优雅的做法是定义一个公共接口模块,base持有接口,动态模块实现接口,通过ServiceLoader或接口注入的方式解耦。第三个坑是国内分发渠道不支持AAB,如果应用需要同时上架国内商店,动态功能模块的代码仍然会打进渠道包,Feature Delivery带来的体积优化只在Google Play渠道生效,国内版本要考虑用插件化框架作为替代方案。
最后,做好下载失败的兜底体验很重要。网络异常、存储不足、用户取消都会导致安装失败,务必根据SplitInstallErrorCode给出明确提示,并在界面上提供重试入口。同时建议在模块下载前向用户说明功能大小和用途,避免消耗流量引发差评。整体而言,Feature Delivery适合出海应用和包体压力大的产品,接入成本主要集中在模块拆分和依赖治理上,一旦架构理顺,后续新增功能都可以按需挂载,收益是长期性的。
Feature Delivery动态功能模块App Bundle修改时间:2026-09-05 13:22:51