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 中,但底层仍保持单例。
DataStore 单例模式结构图 Activity / Fragment ViewModel Worker / Service DataStoreManager (object 单例) by lazy 委托 线程安全初始化 文件系统 app_settings.preferences_pb 单例保证 无论多少调用方请求,最终只返回同一个 DataStore 实例 by lazy 确保首次访问加锁,后续直接返回缓存