第三章 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 初始化注意事项
即使单例实现正确,以下三个初始化细节仍会频繁导致线上问题:
- 必须在主线程调用:
SplitInstallManagerFactory.create()内部会检查Looper,在非主线程抛出IllegalStateException。 - 尽早初始化:推荐在
Application.onCreate()中触发一次单例的获取,确保后续任意位置调用时 Manager 已就绪。 - 版本兼容性检查: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 Context | Activity 关闭后回调无响应,或 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 的完整流程,突出上下文传递与单例检查。
3.8 部署核对清单
- ☐ 确认所有
context传入处都调用了.applicationContext - ☐ 在
Application.onCreate()中至少调用一次SplitInstallManagerHolder.get(this) - ☐ 用
try-catch包裹初始化代码,兼容低版本 Play Store - ☐ 使用
@Volatile和synchronized实现 DCL 单例 - ☐ 通过日志验证 Manager 非 null,且
deferredModules返回正常
遵循以上实现,即可获得一个稳定、无泄漏且线程安全的 SplitInstallManager 实例,为后续模块下载与安装任务打下坚实基础。