DataStore vs SharedPreferences 迁移指南:告别 ANR,拥抱类型安全
DataStore vs SharedPreferences 迁移指南告别 ANR拥抱类型安全一句话收益掌握从 SharedPreferences 迁移到 Jetpack DataStore 的完整路径彻底消除主线程 I/O 阻塞与类型安全隐患。适用版本Android API 21DataStore 1.1.xKotlin 1.9阅读时长约 18 分钟1. 从一次线上 ANR 说起某电商 App 的启动链路中有一段看似无害的代码// 在 Application.onCreate() 中读取用户配置valprefsgetSharedPreferences(user_config,Context.MODE_PRIVATE)valisLoggedInprefs.getBoolean(is_logged_in,false)valuserIdprefs.getString(user_id,)?:上线后Crash 平台开始收到 ANR 报告堆栈始终指向SharedPreferencesImpl.getBoolean()。原因很简单SharedPreferences 在首次加载时会将整个 XML 文件同步读入内存若文件较大存储了用户历史 SKU 缓存等主线程在等待 I/O 完成前会被阻塞。这个场景催生了今天的主题——Jetpack DataStore以及如何安全地完成迁移。2. SharedPreferences 的三个根本缺陷2.1 主线程 I/O 阻塞Application.onCreate() └─ getSharedPreferences() ← 触发磁盘读取 └─ SharedPreferencesImpl() └─ startLoadFromDisk() ← 异步加载但 get*() 会等待完成 └─ awaitLoadedLocked() ← 主线程 wait()ANR 风险AOSP 源码路径frameworks/base/core/java/android/app/SharedPreferencesImpl.java关键方法SharedPreferencesImpl#awaitLoadedLocked()2.2 apply() 的隐式延迟提交// 看起来异步实则有坑prefs.edit().putString(key,value).apply()apply()会将写操作提交到内存并安排异步磁盘写入但Activity.onStop() 会等待所有 apply() 完成通过QueuedWork.waitToFinish()。高频写入场景下这里同样是 ANR 温床。AOSP 路径frameworks/base/core/java/android/app/QueuedWork.java2.3 无类型安全保证// 编译时不报错运行时 ClassCastExceptionprefs.putInt(threshold,100)// 某处误读为 Stringvalthresholdprefs.getString(threshold,0)// crash!3. DataStore 架构总览DataStore 提供两种实现DataStore接口 ├── Preferences DataStore ← 无 Schema迁移 SP 的首选 │ └── PreferencesDataStore单文件 Proto 序列化 └── Proto DataStore ← 强类型需定义 .proto Schema └── ProtoDataStoreProtobuf 调用链路Preferences DataStore Caller └── DataStore.data: FlowPreferences └── SingleProcessDataStore └── FileStorage读协程 IO 线程 └── PreferencesSerializerProto/JSON 序列化 写入链路 Caller └── DataStore.edit { prefs - prefs[key] value } └── 在 Dispatchers.IO 上执行返回 suspend 结果关键差异对比维度SharedPreferencesPreferences DataStore线程模型同步含主线程风险协程 Flow完全异步类型安全无String/Int/Boolean 混用通过Preferences.KeyT保障事务性apply/commit 非原子edit {} 块原子提交错误处理无静默失败Flow 异常传播跨进程MODE_MULTI_PROCESS已废弃不支持需 Proto ContentProvider迁移—内置 SharedPreferencesMigration4. Preferences DataStore 实战4.1 依赖引入// build.gradle.kts (Module)dependencies{implementation(androidx.datastore:datastore-preferences:1.1.1)// Proto DataStore可选// implementation(androidx.datastore:datastore:1.1.1)}4.2 创建 DataStore 单例// ❌ 错误写法每次调用都创建新实例导致多实例写入冲突fungetDataStore(context:Context):DataStorePreferencesPreferenceDataStoreFactory.create{context.preferencesDataStoreFile(user_prefs)}// ✅ 正确写法顶层委托属性保证全局唯一实例valContext.userPrefsDataStore:DataStorePreferencesbypreferencesDataStore(nameuser_prefs)问题说明DataStore 的文件操作依赖单一SingleProcessDataStore实例维护写入队列。多实例会导致并发写入覆盖数据损坏。原理preferencesDataStore委托内部使用DataStoreSingletonDelegate通过synchronizedHashMap保证每个文件名只有一个实例。4.3 定义类型安全的 KeyobjectUserPrefsKeys{valIS_LOGGED_INbooleanPreferencesKey(is_logged_in)valUSER_IDstringPreferencesKey(user_id)valTHEME_MODEintPreferencesKey(theme_mode)// 0跟系统, 1浅色, 2深色valLAST_SYNC_TIMESTAMPlongPreferencesKey(last_sync_ts)}4.4 读取数据FlowclassUserPrefsRepository(privatevaldataStore:DataStorePreferences){// 读取单个值带默认值valisLoggedIn:FlowBooleandataStore.data.catch{e-// 处理 IOException文件损坏等if(eisIOException){emit(emptyPreferences())}elsethrowe}.map{prefs-prefs[UserPrefsKeys.IS_LOGGED_IN]?:false}// 读取多个值组合成数据类dataclassUserConfig(valisLoggedIn:Boolean,valuserId:String,valthemeMode:Int)valuserConfig:FlowUserConfigdataStore.data.catch{if(itisIOException)emit(emptyPreferences())elsethrowit}.map{prefs-UserConfig(isLoggedInprefs[UserPrefsKeys.IS_LOGGED_IN]?:false,userIdprefs[UserPrefsKeys.USER_ID]?:,themeModeprefs[UserPrefsKeys.THEME_MODE]?:0)}}4.5 写入数据suspendfunsetLoggedIn(userId:String){dataStore.edit{prefs-// edit {} 块是事务性的要么全部成功要么全部回滚prefs[UserPrefsKeys.IS_LOGGED_IN]trueprefs[UserPrefsKeys.USER_ID]userId prefs[UserPrefsKeys.LAST_SYNC_TIMESTAMP]System.currentTimeMillis()}}suspendfunlogout(){dataStore.edit{prefs-prefs.remove(UserPrefsKeys.USER_ID)prefs[UserPrefsKeys.IS_LOGGED_IN]false}}// ❌ 错误写法在非挂起上下文中调用阻塞线程funsetThemeSync(mode:Int){runBlocking{dataStore.edit{it[UserPrefsKeys.THEME_MODE]mode}}// runBlocking 在主线程调用会 ANR与使用 SP 无本质区别}// ✅ 正确写法在 ViewModel 协程中调用funsetTheme(mode:Int){viewModelScope.launch{userPrefsRepository.setThemeMode(mode)}}5. 从 SharedPreferences 迁移的完整方案5.1 内置迁移器DataStore 提供SharedPreferencesMigration只在首次访问 DataStore 时执行一次完成后自动删除旧 SP 文件可配置保留。valContext.userPrefsDataStore:DataStorePreferencesbypreferencesDataStore(nameuser_prefs,produceMigrations{context-listOf(SharedPreferencesMigration(contextcontext,sharedPreferencesNameuser_config,// 旧 SP 文件名// 可选仅迁移指定 keykeysToMigratesetOf(is_logged_in,user_id,theme_mode),// 可选迁移后保留旧 SP 文件默认 false即删除// deleteEmptyPreferences true))})迁移流程首次调用 dataStore.data └── DataStoreMigrationUtils.runMigrations() └── SharedPreferencesMigration.shouldMigrate() // 检查 SP 文件是否存在 └── migrate(currentData, spData) // 合并数据 └── 写入 DataStore标记迁移完成 └── deleteEmptyPreferences → 删除旧 SP 文件5.2 多 SP 文件场景若项目中存在多个 SP 文件建议分批迁移或合并到单一 DataStoreproduceMigrations{context-listOf(SharedPreferencesMigration(context,user_config),SharedPreferencesMigration(context,app_settings),SharedPreferencesMigration(context,feature_flags))}注意多个迁移器按顺序执行每个都是独立事务。5.3 自定义 Key 映射旧 Key → 新 Key旧 SP 使用不规范 key 名如驼峰、带空格迁移时可重命名SharedPreferencesMigration(contextcontext,sharedPreferencesNameuser_config,migrate{spData,currentData-valmutablePrefscurrentData.toMutablePreferences()// 旧 key: UserIsLoggedIn → 新 key: is_logged_inspData.getBoolean(UserIsLoggedIn,false).let{mutablePrefs[UserPrefsKeys.IS_LOGGED_IN]it}spData.getString(UserId,)?.let{mutablePrefs[UserPrefsKeys.USER_ID]it}mutablePrefs.toPreferences()})5.4 迁移验证策略// 在 Debug 构建中迁移后对比两端数据if(BuildConfig.DEBUG){valoldPrefscontext.getSharedPreferences(user_config,Context.MODE_PRIVATE)valnewPrefscontext.userPrefsDataStore.data.first()check(oldPrefs.getBoolean(is_logged_in,false)(newPrefs[UserPrefsKeys.IS_LOGGED_IN]?:false)){Migration verification failed for IS_LOGGED_IN}}6. 在 ViewModel 与 Hilt 中集成6.1 Hilt 注入 DataStoreModuleInstallIn(SingletonComponent::class)objectDataStoreModule{ProvidesSingletonfunprovideUserPrefsDataStore(ApplicationContextcontext:Context):DataStorePreferencescontext.userPrefsDataStore}HiltViewModelclassSettingsViewModelInjectconstructor(privatevaluserPrefsRepo:UserPrefsRepository):ViewModel(){valthemeMode:StateFlowIntuserPrefsRepo.themeMode.stateIn(scopeviewModelScope,startedSharingStarted.WhileSubscribed(5_000),initialValue0)funonThemeSelected(mode:Int){viewModelScope.launch{userPrefsRepo.setThemeMode(mode)}}}6.2 在 Compose 中消费ComposablefunSettingsScreen(viewModel:SettingsViewModelhiltViewModel()){valthemeModebyviewModel.themeMode.collectAsStateWithLifecycle()ThemeSelector(selectedthemeMode,onSelectviewModel::onThemeSelected)}7. 常见坑点坑 1在 Application.onCreate() 中同步读取 DataStore现象升级 DataStore 后启动崩溃LogCat 报IllegalStateException: Cannot invoke suspend function from non-suspend context。原因DataStore 所有读写均为 suspend 函数不能在非协程上下文同步调用。复现// ❌ 在 Application.onCreate() 中同步读取classMyApp:Application(){overridefunonCreate(){super.onCreate()valprefsrunBlocking{userPrefsDataStore.data.first()}// 主线程 blockif(prefs[UserPrefsKeys.IS_LOGGED_IN]true){...}}}解决将逻辑移到首个 Activity/Fragment 的协程中或使用Application级CoroutineScopeclassMyApp:Application(),CoroutineScopebyMainScope(){overridefunonCreate(){super.onCreate()launch{valprefsuserPrefsDataStore.data.first()// 异步处理}}}坑 2迁移后旧 SP 文件仍被某处代码访问现象用户数据在 DataStore 和 SP 之间出现不一致。原因代码中仍有旧的getSharedPreferences(user_config, ...)调用绕过了 DataStore。复现迁移后的老代码路径如 WebView Bridge、旧 Fragment未同步更新。解决用 Lint 规则强制拦截 SP 调用// 在 lint.xml 中禁用 SharedPreferences 用法issue idCommitPrefEditsseverityerror/// 或自定义 Lint 规则检测 getSharedPreferences() 调用坑 3DataStore 文件损坏后无法恢复现象极少数情况下存储空间不足、进程被 kill导致 Proto 文件损坏dataFlow 持续抛出异常。原因DataStore 的 CorruptionHandler 未配置默认重抛异常。解决配置ReplaceFileCorruptionHandlervalContext.userPrefsDataStore:DataStorePreferencesbypreferencesDataStore(nameuser_prefs,corruptionHandlerReplaceFileCorruptionHandler{emptyPreferences()}// 损坏时重置为空数据丢失但不崩溃)坑 4跨进程场景直接使用 Preferences DataStore现象多进程 App如有 :push 进程同时读写 DataStore数据丢失或 IOException。原因SingleProcessDataStore不支持多进程并发写入文件锁基于 JVM 实例。解决使用ContentProvider封装 DataStore或改用 Room支持 WAL 模式多进程安全。8. 最佳实践8.1 Repository 模式封装隔离 DataStore 细节做法通过 Repository 接口暴露 Flow 和 suspend 函数ViewModel 不直接持有 DataStore 引用。原因便于单元测试mock Repository且迁移到 Proto DataStore 或其他存储时无需修改 ViewModel 层。对比若 ViewModel 直接调用dataStore.edit {}测试时需启动真实文件系统测试速度慢 10 倍以上。8.2 使用stateIn缓存 Flow避免多次订阅重复读文件做法valisLoggedIn:StateFlowBooleanrepo.isLoggedIn.stateIn(viewModelScope,SharingStarted.Eagerly,false)原因Flow 默认是冷流每次collect都会重新读取。stateIn将其转为热流多个 Composable 订阅共享同一数据减少 I/O。对比不用stateIn时3 个 Composable 订阅同一配置项 3 次磁盘读取触发。8.3 批量写入时使用单个edit {}块做法// ✅ 一个 edit 块 一次磁盘写入dataStore.edit{prefs-prefs[KEY_A]valueA prefs[KEY_B]valueB prefs[KEY_C]valueC}// ❌ 三次独立 edit 三次磁盘写入dataStore.edit{it[KEY_A]valueA}dataStore.edit{it[KEY_B]valueB}dataStore.edit{it[KEY_C]valueC}原因每次edit {}都是一次完整的读-改-写流程合并写入降低 IOPS延长闪存寿命。8.4 大数据量不适合 DataStore做法超过 100KB 的数据如缓存列表、图片路径集合改用 Room 或文件存储。原因DataStore 每次写入都会全量序列化整个 Preferences 对象大数据量时 CPU 开销显著。对比Room 支持增量更新1000 条记录更新单条耗时 1msDataStore 全量写入同等数据需 20~50ms。9. 总结SharedPreferences 的主线程 I/O 与非原子提交是 ANR 的根源DataStore 从架构层面消除了这两个风险。preferencesDataStore委托属性是创建单例的唯一正确姿势多实例会导致数据损坏。内置SharedPreferencesMigration提供零代码迁移路径支持 Key 重映射与数据过滤。DataStore 不支持跨进程多进程场景需借助 ContentProvider 或改用 Room。配置ReplaceFileCorruptionHandler是生产环境的必选项防止文件损坏导致 App 不可用。核心结论DataStore 不是 SharedPreferences 的简单替换而是对持久化 KV 存储的重新设计——将 I/O 安全性、类型安全、错误处理的责任从调用者转移到了框架本身。参考资料DataStore 官方文档从 SharedPreferences 迁移到 DataStoreDataStore 设计文档MediumAOSP 源码frameworks/base/core/java/android/app/SharedPreferencesImpl.javaAOSP 源码frameworks/base/core/java/android/app/QueuedWork.javaDataStore 源码androidx/datastore/core/SingleProcessDataStore.kt