第三章 SplitInstallManager 获取:上下文依赖、单例模式、初始化注意事项

使用 Play Core SplitInstall 的第一步是获取 SplitInstallManager 实例。这个入口看似简单,但背后涉及上下文生命周期、单例线程安全以及初始化时机等关键问题。本章将拆解这三个要点,并给出经过验证的实现与避坑方案。

3.1 上下文依赖原理

SplitInstallManager 的所有 API 都需要绑定一个 Context,原因在于内部会通过 PackageManager 查询已安装模块、通过 PendingIntent 触发系统安装流程。错误的上下文选择可能导致内存泄漏或回调丢失。

上下文类型是否推荐原因
ApplicationContext强烈推荐与应用进程同生命周期,不会泄漏 Activity
Activity.this不推荐Activity 销毁后 Manager 仍可能被持有,导致泄漏
Service.this可用但非最优Service 生命周期短于 Application,不建议长期持有
关键原则:始终使用 applicationContext。如果从 Activity 中获取,调用 context.applicationContext 转换。

3.2 单例模式实现

SplitInstallManager 本身是轻量级对象,但为了避免重复创建和底层资源浪费,最佳实践是将其封装为应用全局单例。推荐使用 双重检查锁定(DCL) 方案,兼顾线程安全与性能。

class SplitInstallManagerHolder private constructor() {

    companion object {
        @Volatile
        private var instance: SplitInstallManager? = null

        fun get(context: Context): SplitInstallManager {
            return instance ?: synchronized(this) {
                instance ?: SplitInstallManagerFactory.create(context.applicationContext)
                    .also { instance = it }
            }
        }
    }
}

为什么选择 DCL?

  • 线程安全:synchronized 保证首次创建时只有一个线程进入,@Volatile 确保写操作立即可见。
  • 懒加载:只在首次调用时创建,避免应用启动时的性能开销。
  • 无 Activity 泄漏:传入 applicationContext,不持有 Activity 引用。
替代方案:如果项目已使用 Hilt / Dagger,可以直接将 SplitInstallManager 作为 @Singleton 提供,但底层仍建议通过 applicationContext 创建。

3.3 初始化注意事项

即使单例实现正确,以下三个初始化细节仍会频繁导致线上问题:

  1. 必须在主线程调用:SplitInstallManagerFactory.create() 内部会检查 Looper,在非主线程抛出 IllegalStateException。
  2. 尽早初始化:推荐在 Application.onCreate() 中触发一次单例的获取,确保后续任意位置调用时 Manager 已就绪。
  3. 版本兼容性检查:Play Core 1.10.0+ 要求 SplitInstallManager 与 Play Store 版本匹配,低版本设备可能返回 null 或抛出异常,需做安全捕获。
// Application.kt
override fun onCreate() {
    super.onCreate()
    // 触发初始化,捕获可能的版本不兼容异常
    try {
        SplitInstallManagerHolder.get(this)
    } catch (e: Exception) {
        Log.w("SplitInstall", "Manager init failed: ${e.message}")
    }
}

3.4 调试与验证

初始化完成后,可通过日志确认 Manager 是否正常工作:

val manager = SplitInstallManagerHolder.get(context)
Log.d("SplitInstall", "Manager created: $manager")
Log.d("SplitInstall", "Deferred modules: ${manager.deferredModules}")

在 Logcat 中过滤 SplitInstall 标签,观察输出。若 manager.deferredModules 为空列表,说明 Play Core 初始化成功;若抛出 RuntimeException,则检查上下文是否为 applicationContext。

3.5 常见错误与修复

错误场景表现解决方案
传入 Activity ContextActivity 关闭后回调无响应,或 ContextWrapper 泄露改为 context.applicationContext
在非主线程调用崩溃:Can't create handler inside thread that has not called Looper.prepare()确保调用发生在主线程,或 Handler(Looper.getMainLooper()) 中执行
未在 Application 初始化某些组件(如 WorkManager)中 Manager 为 null在 Application.onCreate 中触发一次单例
Play Core 版本过低SplitInstallManagerFactory.create() 返回 null升级到 1.12.x+,或添加 try-catch

3.6 性能优化建议

  • 延迟非必要调用:首次获取 Manager 后,如果不需要立即使用,可暂不调用 deferredModules 等 API,避免触发底层 PackageManager 查询。
  • 复用 applicationContext:在整个进程生命周期内,只保留一份 applicationContext 的弱引用或直接由 Application 自身提供。
  • 避免频繁 get():单例已经保证了唯一性,但每次调用 SplitInstallManagerHolder.get() 仍有一次 null 判空开销,可考虑将 Manager 存入 Application 的成员变量,直接访问。

3.7 核心流程 SVG 图

下图展示了从 Application 启动到获取可用的 SplitInstallManager 的完整流程,突出上下文传递与单例检查。

SplitInstallManager 获取流程 Application.onCreate() context.applicationContext 单例检查:instance ?: synchronized 双重检查锁定(DCL) Instance 已存在? 返回已有实例 SplitInstallManagerFactory .create(applicationContext) 说明:主线程要求、尽早初始化、异常捕获已在 Application.onCreate 中处理;单例保证线程安全。

3.8 部署核对清单

  • ☐ 确认所有 context 传入处都调用了 .applicationContext
  • ☐ 在 Application.onCreate() 中至少调用一次 SplitInstallManagerHolder.get(this)
  • ☐ 用 try-catch 包裹初始化代码,兼容低版本 Play Store
  • ☐ 使用 @Volatile 和 synchronized 实现 DCL 单例
  • ☐ 通过日志验证 Manager 非 null,且 deferredModules 返回正常

遵循以上实现,即可获得一个稳定、无泄漏且线程安全的 SplitInstallManager 实例,为后续模块下载与安装任务打下坚实基础。