4. DataStore 单例模式:通过属性委托实现全局单例
4.1 原理
DataStore 实例本质上是文件 I/O + 协程的封装,每个实例对应一个文件。如果同一文件被多次创建实例,会导致文件锁冲突、数据不一致甚至崩溃。单例模式确保整个应用生命周期内,一个文件只有一个 DataStore 实例。
Android / Kotlin 推荐通过 委托属性(Delegated Properties) 实现懒加载单例:使用 by lazy 并将初始化逻辑放入 object 或 companion object 中。这样既能延迟创建,又能保证线程安全(lazy 默认的 LazyThreadSafetyMode.SYNCHRONIZED)。
核心要点:属性委托 + 协程 + 文件锁 -> 全局唯一、线程安全的 DataStore 实例。
4.2 实现流程
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 定义全局容器 | 使用 object 或 companion object 持有 DataStore 引用 |
| 2 | 属性委托 | val dataStore: DataStore<Preferences> by lazy { … } |
| 3 | 指定上下文和文件名 | context.dataStoreFile("my_prefs.preferences_pb") |
| 4 | 提供公开访问 | 对外暴露 instance 或直接使用顶层属性 |
| 5 | 验证唯一性 | 对比两次调用返回的实例是否 == 同一对象 |
4.3 代码实现
4.3.1 推荐写法:顶层扩展属性(模块级单例)
// DataStoreExtensions.kt
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
private val Context.dataStore: DataStore<Preferences> by lazy {
preferencesDataStore(name = "app_settings")
}
// 在 Activity / Fragment / ViewModel 中通过上下文访问
// context.dataStore 始终返回同一个实例
4.3.2 经典单例容器(Application 级)
// DataStoreManager.kt
object DataStoreManager {
private lateinit var appContext: Context
fun init(context: Context) {
appContext = context.applicationContext
}
val dataStore: DataStore<Preferences> by lazy {
appContext.dataStore
}
}
// Application 中初始化
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
DataStoreManager.init(this)
}
}
// 任意位置调用
DataStoreManager.dataStore.edit { ... }
4.3.3 带协程作用域的单例
// 确保单例与协程生命周期一致
object DataStoreSingleton {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val dataStore: DataStore<Preferences> by lazy {
preferencesDataStore(name = "secure_data")
}
fun dispose() {
scope.cancel()
}
}
4.4 调试技巧
- 检查实例数量:在初始化处加日志,输出
this.hashCode(),确认每次调用返回相同值。 - 文件名冲突:不同的 DataStore 文件必须使用不同名称,否则单例模式会失效(
preferencesDataStore内部按 name 缓存)。 - 线程安全测试:同时从多个线程首次访问,使用
lazy默认同步锁,不会出现重复创建。
4.5 部署注意事项
- 将单例对象放在公共模块(如
common或core)中,避免在多个模块中重复定义。 - 如果使用 Hilt / Koin 注入,可以配置为
@Singleton(本质仍是委托单例)。 - 不要将单例的生命周期与 Activity/Fragment 绑定,始终使用 Application Context。
4.6 常见错误
- 多次调用
preferencesDataStore顶级扩展:该扩展本身已做文件级缓存,但若在多个类中独立声明不同 name,将创建多个不同文件。 - 在非 Application Context 上创建:使用 Activity Context 可能导致内存泄漏(但 DataStore 本身会持有引用,建议只传 Application)。
- 忘记初始化:调用
DataStoreManager.dataStore前未调用init(),抛出UninitializedPropertyAccessException。
避坑:不要在
lazy 委托块中捕获即将被回收的 Context 引用。始终使用 applicationContext。
4.7 性能与稳定性优化
- 预初始化:在
Application.onCreate()中主动访问一次单例(例如DataStoreManager.dataStore),避免首次 I/O 操作时的显示卡顿。 - 使用
LazyThreadSafetyMode.PUBLICATION:如果你确定初始化逻辑是幂等的(DataStore 创建本身已带锁),可改用by lazy(LazyThreadSafetyMode.PUBLICATION)提升并发性能。 - 内存占用:单例实例只在内存中存一份,节省开销,也避免文件锁竞争。
- 文件损坏恢复:单例不会直接修复文件,但配合
catch块重启 DataStore 时可以重新创建一个。
最佳实践:始终使用
by lazy + Application Context 组合,既保证线程安全,又达到懒加载效果。如果需要更细粒度的控制,可封装在 ViewModel 中,但底层仍保持单例。