4. 单例封装与上下文解耦:创建DataStore实例、避免内存泄漏、Application注入

在生产环境中,如果每次使用DataStore都通过context.createDataStore()创建新实例,会带来两个严重问题:

  1. 多实例竞争——多个实例操作同一个文件会导致数据不一致甚至IOException;
  2. 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 实现流程:三步完成单例封装

  1. 自定义Application类——在onCreate()完成时暴露Application实例;
  2. 编写DataStoreManager单例——内部使用context.dataStore委托属性,并使用synchronized代码块保护初始化;
  3. 提供顶层扩展函数——给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 单例封装结构图

单例封装:Application注入 + 双重校验锁 Application onCreate() 持自身实例 DataStoreManager (object) 双重校验锁 getApplicationContext() 暴露 DataStore<Preferences> applicationContext DataStore<Preferences> lazy 单例 内部持有 ViewModel / Compose DataStoreManager.getString() 无Context调用 user_preferences 磁盘文件 ⚠ 所有使用方零 Context 依赖 生命周期随 Application 同步

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实例由容器统一管理,无需自己写双重校验锁。