SplitInstall动态交付稳定性提升方法 - 第4节

4.1 状态机模型与生命周期契约

Split Install 的运行时行为本质上是一个确定性的有限状态机(FSM)。每个模块安装请求从发起开始,严格遵循单一方向的状态迁移,直到到达终态(INSTALLED / FAILED / CANCELED)。理解这个状态机是编写稳定回调处理逻辑的前提。

核心原则: Splite Install 状态机是不可逆的。一旦进入 FAILED 状态,必须重新发起安装流程;不能从 DOWNLOADED 回退到 DOWNLOADING。所有状态回调都由 Play Core 的异步 Binder 通道驱动,调用方无法干预状态流转顺序。

完整的状态枚举如下表所示:

状态常量整形值语义说明
UNKNOWN0初始占位状态,未发起任何请求或状态未同步
PENDING1请求已提交,等待 Play 商店调度(含用户确认流程)
DOWNLOADING2正在下载模块资源,可通过 onProgress 获得进度
DOWNLOADED3下载完成,但尚未触发安装流程(延迟安装/按需安装的关键状态)
INSTALLING4拷贝与验证文件、更新 Android 包管理记录
INSTALLED5模块已就绪,应用可在运行时访问新资源/代码
FAILED6不可恢复的终态,必须通过 errorCode() 获取具体失败原因
CANCELED7用户或系统取消了本次安装请求
Split Install 状态机迁移图 Split Install 状态机 IDLE / UNKNOWN PENDING DOWNLOADING FAILED DOWNLOADED INSTALLING INSTALLED deferredInstall() onStart() onComplete() 确认安装 onComplete() onError() onError() / 系统终止 manager.startInstall() 重试 状态机关键约束 执行上述重试之前,必须调用 manager.getSessionStates() 确认前序 Session 已释放,否则会触发 ACTIVE_SESSIONS_LIMIT_EXCEEDED 错误。

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_EXCEEDED1同一时刻发起了超过 1 个 startInstall 请求调用 manager.getSessionStates() 等待前序 Session 结束再重试
MODULE_UNAVAILABLE2请求的 moduleName 不在 Play Console 发布包中检查 SplitInstallManager.getInstalledModules(),动态隐藏对应功能入口
NETWORK_ERROR3Wi-Fi / 蜂窝网络不可用或不稳定监听 ConnectivityManager,网络恢复后自动调用 startInstall
INCOMPATIBLE_WITH_EXISTING_SESSION4新模块的存储类型(APK / Asset Pack)与当前队列冲突强制取消当前队列 manager.cancelInstall(sessionId) 后重试
INTERNAL_ERROR6Play 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 不一致或功能不可用。这是动态交付稳定性的最后一道闭环防线。