4. 单例封装与上下文解耦:创建DataStore实例、避免内存泄漏、Application注入
在生产环境中,如果每次使用DataStore都通过context.createDataStore()创建新实例,会带来两个严重问题:
- 多实例竞争——多个实例操作同一个文件会导致数据不一致甚至
IOException; - Context泄漏——持有Activity或Fragment的Context会使对象无法被GC回收。
本章通过单例模式+Application注入,保证全局唯一DataStore实例,同时将Context引用限制在Application生命周期内。
常见错误:在Application.onCreate()中直接创建DataStore实例尚未完成,模块之间依赖顺序可能导致空指针。本章选用延迟初始化+双重校验锁的方式解决该问题。
4.1 两种主流实现方案对比
| 方案 | 实现方式 | 线程安全 | Context依赖 | 推荐场景 |
|---|---|---|---|---|
| Application注入 | 自定义Application持有Context引用,DataStore构造时传入 | 依赖synchronized/volatile | 强引用但生命周期一致 | 主项目、组件化主壳 |
| 依赖注入容器(Hilt/Koin) | 由DI框架管理单例生命周期 | 由框架保证 | 弱引用/作用域限定 | 已有DI体系的项目 |
| 静态持有+反射获取Application | 通过ActivityThread或ContentProvider初始化 | 需额外处理 | 静态且隐匿 | SDK、library模块 |
本章重点讲解Application注入 + 双重校验锁的纯手动实现方式,不依赖任何第三方框架。
4.2 实现流程:三步完成单例封装
- 自定义Application类——在
onCreate()完成时暴露Application实例; - 编写DataStoreManager单例——内部使用
context.dataStore委托属性,并使用synchronized代码块保护初始化; - 提供顶层扩展函数——给Compose/ViewModel使用者提供无Context调用的入口。
下面接入完整代码实现。
4.3 完整代码实现
第一步:自定义Application
class App : Application() {
companion object {
@Volatile
private lateinit var instance: App
fun getInstance(): App = instance
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
第二步:单例DataStoreManager
object DataStoreManager {
private const val PREFS_NAME = "user_preferences"
// 延迟初始化,使用Application级别的Context,避免泄漏
private val dataStore: DataStore<Preferences> by lazy {
getApplicationContext().let { context ->
context.createDataStore(name = PREFS_NAME)
}
}
// 双重校验锁确保只初始化一次Context引用
@Volatile
private var appContext: Context? = null
private fun getApplicationContext(): Context {
return appContext ?: synchronized(this) {
appContext ?: App.getInstance().applicationContext.also {
appContext = it
}
}
}
// ------------- 对外暴露的操作API -------------
suspend fun saveString(key: String, value: String) {
dataStore.edit { prefs ->
prefs[stringPreferencesKey(key)] = value
}
}
suspend fun getString(key: String): Flow<String?> {
return dataStore.data.map { prefs ->
prefs[stringPreferencesKey(key)]
}
}
fun getPreferences(): DataStore<Preferences> = dataStore
}
第三步:在AndroidManifest.xml中声明
<application
android:name=".App"
android:icon="@mipmap/ic_launcher"
... >
...
</application>
关键点:使用
App.getInstance().applicationContext而非this。applicationContext是全局单例,永远不会泄漏,它的生命周期等于整个App进程。
4.4 生命周期与内存泄漏分析
对比两种不好的写法:
| 错误写法 | 泄漏原因 | 后果 |
|---|---|---|
context.createDataStore()(Activity Context) |
Activity销毁后DaataStore仍持有Context | Activity无法GC,内存泄漏 |
| 在静态字段直接存Activity Context | 静态生命周期 > Activity生命周期 | 持有旧Activity导致View泄漏 |
正确方式:持有Application Context且仅通过lazy初始化一次,这是Android官方推荐的避免泄漏的标准模式。
4.5 单例封装结构图
4.6 如何验证实例唯一性
在Application.onCreate()和ViewModel初始化时打日志,确认DataStore
对象的hashCode()是否一致:
// 在DataStoreManager单例中添加调试代码
init {
Log.d("DataStoreManager", "DataStore instance hash: ${dataStore.hashCode()}")
}
// 在其他位置调用多次,检查是否同一对象
repeat(3) {
Log.d("Test", "hash: ${DataStoreManager.getPreferences().hashCode()}")
}
预期输出三个相同的hash值。
4.7 调试与边界处理
- 测试环境中的Application: Robolectric或Instrumentation测试中可使用自定义
TestApplication,继承真正的App或mock必要的Context; - 多进程场景: 默认DataStore不支持多进程读写。如果App有多个进程,每个进程需要使用独立的DataStore文件路径,或在Application中判断进程名;
- Android 12+ Partitioned Storage: DataStore文件存于
internal files,不受分区存储影响,无需额外适配。
4.8 性能与稳定性优化
| 优化项 | 具体措施 | 收益 |
|---|---|---|
| 减少锁竞争 | volatile + synchronized仅用于首次Context初始化,之后走快速路径 |
读操作无锁开销 |
| 避免文件过大 | 定期清理过期key,或拆分多个DataStore文件(如settings、cache) | 缩短序列化/反序列化时间 |
| 异常隔离 | 在dataStore.data收集时添加catch,返回默认值而非崩溃 |
IO异常不传导至UI层 |
| 懒加载注入 | 对于非必需模块,不要强制在Application.onCreate中初始化DataStore | 减少冷启动耗时 |
推荐:如果项目已接入Hilt或Koin,优先使用
@Singleton + @ApplicationContext,DataStore实例由容器统一管理,无需自己写双重校验锁。