SplitInstall动态交付稳定性提升方法 - 第4节
4.1 状态机模型与生命周期契约
Split Install 的运行时行为本质上是一个确定性的有限状态机(FSM)。每个模块安装请求从发起开始,严格遵循单一方向的状态迁移,直到到达终态(INSTALLED / FAILED / CANCELED)。理解这个状态机是编写稳定回调处理逻辑的前提。
核心原则: Splite Install 状态机是不可逆的。一旦进入 FAILED 状态,必须重新发起安装流程;不能从 DOWNLOADED 回退到 DOWNLOADING。所有状态回调都由 Play Core 的异步 Binder 通道驱动,调用方无法干预状态流转顺序。
完整的状态枚举如下表所示:
| 状态常量 | 整形值 | 语义说明 |
|---|---|---|
UNKNOWN | 0 | 初始占位状态,未发起任何请求或状态未同步 |
PENDING | 1 | 请求已提交,等待 Play 商店调度(含用户确认流程) |
DOWNLOADING | 2 | 正在下载模块资源,可通过 onProgress 获得进度 |
DOWNLOADED | 3 | 下载完成,但尚未触发安装流程(延迟安装/按需安装的关键状态) |
INSTALLING | 4 | 拷贝与验证文件、更新 Android 包管理记录 |
INSTALLED | 5 | 模块已就绪,应用可在运行时访问新资源/代码 |
FAILED | 6 | 不可恢复的终态,必须通过 errorCode() 获取具体失败原因 |
CANCELED | 7 | 用户或系统取消了本次安装请求 |
4.2 核心回调接口与线程安全
SplitInstallStateUpdatedListener 是状态机的驱动引擎。回调函数的触发时机如下表所示:
| 回调方法 | 触发时机 | 常见状态值 |
|---|---|---|
onStart() | 下载任务被 Play Store 接受并开始调度 | PENDING |
onProgress() | 有新的字节数据下载完成(频率较高) | DOWNLOADING |
onComplete() | 下载或安装步骤成功完成 | DOWNLOADED / INSTALLED |
onError() | 任何阶段发生不可恢复的错误 | FAILED |
onDeferredInstall() | 延迟安装被触发(条件满足时自动开始) | PENDING → DOWNLOADING |
线程模型陷阱: 所有回调默认在 Play Core 的内部 Binder 线程池中执行。在回调中直接更新 UI 或访问非线程安全的对象(如 SharedPreferences 编辑器)会导致偶发崩溃。必须使用 mainHandler.post(() -> updateUI(state)) 将操作切换到主线程。
// 标准回调注册示例
SplitInstallStateUpdatedListener listener = new SplitInstallStateUpdatedListener() {
@Override
public void onStateUpdate(@NonNull SplitInstallSessionState state) {
// 立即捕获状态
final int status = state.status();
final int errorCode = state.errorCode();
final long bytesDownloaded = state.bytesDownloaded();
final long totalBytes = state.totalBytesToDownload();
// 切换到主线程处理 UI 和业务逻辑
new Handler(Looper.getMainLooper()).post(() -> {
switch (status) {
case SplitInstallSessionStatus.DOWNLOADING:
int progress = (int) (bytesDownloaded * 100 / totalBytes);
progressBar.setProgress(progress);
break;
case SplitInstallSessionStatus.INSTALLED:
onModuleReady(state.moduleNames());
break;
case SplitInstallSessionStatus.FAILED:
handleError(errorCode);
break;
default:
break;
}
});
}
};
// 注册与注销(需在 Activity/Fragment 生命周期中对称调用)
SplitInstallManager manager = SplitInstallManagerFactory.create(context);
manager.registerListener(listener);
// onDestroy 或不再需要时:
// manager.unregisterListener(listener);
4.3 错误码映射与处理策略
错误的处理不应直接展示给用户。应建立统一的错误码映射层,转换为用户友好提示与重试/降级方案。
| 错误码常量 | 整型值 | 根因 | 推荐处理逻辑 |
|---|---|---|---|
ACTIVE_SESSIONS_LIMIT_EXCEEDED | 1 | 同一时刻发起了超过 1 个 startInstall 请求 | 调用 manager.getSessionStates() 等待前序 Session 结束再重试 |
MODULE_UNAVAILABLE | 2 | 请求的 moduleName 不在 Play Console 发布包中 | 检查 SplitInstallManager.getInstalledModules(),动态隐藏对应功能入口 |
NETWORK_ERROR | 3 | Wi-Fi / 蜂窝网络不可用或不稳定 | 监听 ConnectivityManager,网络恢复后自动调用 startInstall |
INCOMPATIBLE_WITH_EXISTING_SESSION | 4 | 新模块的存储类型(APK / Asset Pack)与当前队列冲突 | 强制取消当前队列 manager.cancelInstall(sessionId) 后重试 |
INTERNAL_ERROR | 6 | Play Core 内部逻辑异常(多由 IPC 断开导致) | 建议指数退避重试 3 次,若仍失败则提示“请重试或重启应用” |
稳定性优化: 在 onError 中增加日志聚合,采集 state.errorCode() 与 state.moduleNames() 上报至 APM 系统。长期监控可发现特定型号设备上的高频错误码,驱动 Play Core 版本升级或业务降级策略提前下发。
4.4 多状态组合与并发管理
Android Split Install 默认只允许一个活跃的下载 Session。当应用同时下载多个模块时,系统会将它们合并到同一个 PENDING → DOWNLOADING 队列中。不正确的并发管理是生产环境中最常见的稳定性问题。
- 合并机制: 多次
startInstall(module1)和startInstall(module2)若在时间窗口内触发,Play Store 会合并成同一个 Session。此时回调中的state.moduleNames()会返回列表,不能只取第一个元素。 - 冲突案例: 如果模块 A 正在 DOWNLOADING,模块 B 调用
startInstall()会立刻收到ACTIVE_SESSIONS_LIMIT_EXCEEDED。 - 最佳实践: 在发起下载前统一调用
manager.getSessionStates()检查当前所有 Session 状态。
// 并发安全的下载启动模板
public void startDownloadSafe(String moduleName) {
Task<List<SplitInstallSessionState>> statesTask = manager.getSessionStates();
statesTask.addOnSuccessListener(sessionStates -> {
for (SplitInstallSessionState state : sessionStates) {
if (state.status() == SplitInstallSessionStatus.DOWNLOADING
|| state.status() == SplitInstallSessionStatus.PENDING) {
Log.w(TAG, "已有活跃 Session (" + state.sessionId() + "),等待完成");
return; // 静默等待,不重复发起
}
}
// 没有冲突,发起新下载
manager.startInstall(
SplitInstallRequest.newBuilder()
.addModule(moduleName)
.build()
);
}).addOnFailureListener(e -> {
Log.e(TAG, "查询 Session 状态失败", e);
// 降级:直接发起,捕获 ACTIVE_SESSIONS_LIMIT_EXCEEDED 错误
manager.startInstall(
SplitInstallRequest.newBuilder()
.addModule(moduleName)
.build()
);
});
}
4.5 状态持久化与进程恢复
由于 Split Install 的安装过程可能在后台独立运行,App 可能被系统杀死。如果不做持久化,用户重新打开 App 时状态机将丢失上下文,导致功能入口显示错误、安装进度丢失等问题。
持久化时机: 在 onStateUpdate 回调中,针对关键状态(DOWNLOADED、INSTALLED、FAILED)写入 DataStore 或 SharedPreferences。恢复时机为 Application.onCreate() 或 MainActivity.onCreate()。
// 使用 Jetpack DataStore 进行键值持久化
// StatePersister.kt
class StatePersister(private val context: Context) {
private val Context.dataStore by preferencesDataStore(name = "split_install_state")
private val KEY_MODULE = stringPreferencesKey("restorable_module")
private val KEY_STATUS = intPreferencesKey("restorable_status")
suspend fun save(moduleName: String, status: Int) {
context.dataStore.edit { prefs ->
prefs[KEY_MODULE] = moduleName
prefs[KEY_STATUS] = status
}
}
suspend fun restore(): Pair<String, Int>? {
val prefs = context.dataStore.data.first()
val module = prefs[KEY_MODULE] ?: return null
val status = prefs[KEY_STATUS] ?: return null
return Pair(module, status)
}
suspend fun clear() {
context.dataStore.edit { it.clear() }
}
}
// 在 Application.onCreate() 中恢复
class MyApp : Application() {
val statePersister by lazy { StatePersister(this) }
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.IO).launch {
statePersister.restore()?.let { (module, status) ->
if (status == SplitInstallSessionStatus.DOWNLOADED ||
status == SplitInstallSessionStatus.INSTALLING) {
// 向系统查询真实的 Session 状态,决定是否需要重新安装
val manager = SplitInstallManagerFactory.create(this@MyApp)
manager.getSessionStates().addOnSuccessListener { states ->
val match = states.find { it.moduleNames().contains(module) }
if (match == null || match.status() == SplitInstallSessionStatus.FAILED) {
// 上下文丢失,重新发起安装
manager.startInstall(
SplitInstallRequest.newBuilder().addModule(module).build()
)
} else {
// 已有正确状态,重新注册 listener 继续监听
}
}
}
}
}
}
}
通过以上状态持久化与恢复逻辑,App 可以在被系统回收后仍能完整感知 Split Install 的执行进度,避免“状态穿越”导致的 UI 不一致或功能不可用。这是动态交付稳定性的最后一道闭环防线。