DataStore vs SharedPreferences 迁移指南:告别 ANR,拥抱类型安全
2026/5/23 2:24:36 网站建设 项目流程

DataStore vs SharedPreferences 迁移指南:告别 ANR,拥抱类型安全

一句话收益:掌握从 SharedPreferences 迁移到 Jetpack DataStore 的完整路径,彻底消除主线程 I/O 阻塞与类型安全隐患。
适用版本:Android API 21+,DataStore 1.1.x,Kotlin 1.9+
阅读时长:约 18 分钟


1. 从一次线上 ANR 说起

某电商 App 的启动链路中有一段看似无害的代码:

// 在 Application.onCreate() 中读取用户配置valprefs=getSharedPreferences("user_config",Context.MODE_PRIVATE)valisLoggedIn=prefs.getBoolean("is_logged_in",false)valuserId=prefs.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.java

2.3 无类型安全保证

// 编译时不报错,运行时 ClassCastExceptionprefs.putInt("threshold",100)// 某处误读为 Stringvalthreshold=prefs.getString("threshold","0")// crash!

3. DataStore 架构总览

DataStore 提供两种实现:

DataStore(接口) ├── Preferences DataStore ← 无 Schema,迁移 SP 的首选 │ └── PreferencesDataStore(单文件 Proto 序列化) └── Proto DataStore ← 强类型,需定义 .proto Schema └── ProtoDataStore(Protobuf) 调用链路(Preferences DataStore): Caller └── DataStore.data: Flow<Preferences> └── SingleProcessDataStore └── FileStorage(读:协程 IO 线程) └── PreferencesSerializer(Proto/JSON 序列化) 写入链路: Caller └── DataStore.edit { prefs -> prefs[key] = value } └── 在 Dispatchers.IO 上执行,返回 suspend 结果

关键差异对比

维度SharedPreferencesPreferences DataStore
线程模型同步(含主线程风险)协程 + Flow,完全异步
类型安全无(String/Int/Boolean 混用)通过Preferences.Key<T>保障
事务性apply/commit 非原子edit {} 块原子提交
错误处理无(静默失败)Flow 异常传播
跨进程MODE_MULTI_PROCESS(已废弃)不支持(需 Proto + ContentProvider)
迁移内置 SharedPreferencesMigration

4. 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):DataStore<Preferences>=PreferenceDataStoreFactory.create{context.preferencesDataStoreFile("user_prefs")}// ✅ 正确写法:顶层委托属性,保证全局唯一实例valContext.userPrefsDataStore:DataStore<Preferences>bypreferencesDataStore(name="user_prefs")

问题说明:DataStore 的文件操作依赖单一SingleProcessDataStore实例维护写入队列。多实例会导致并发写入覆盖,数据损坏。
原理preferencesDataStore委托内部使用DataStoreSingletonDelegate,通过synchronized+HashMap保证每个文件名只有一个实例。

4.3 定义类型安全的 Key

objectUserPrefsKeys{valIS_LOGGED_IN=booleanPreferencesKey("is_logged_in")valUSER_ID=stringPreferencesKey("user_id")valTHEME_MODE=intPreferencesKey("theme_mode")// 0=跟系统, 1=浅色, 2=深色valLAST_SYNC_TIMESTAMP=longPreferencesKey("last_sync_ts")}

4.4 读取数据(Flow)

classUserPrefsRepository(privatevaldataStore:DataStore<Preferences>){// 读取单个值,带默认值valisLoggedIn:Flow<Boolean>=dataStore.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:Flow<UserConfig>=dataStore.data.catch{if(itisIOException)emit(emptyPreferences())elsethrowit}.map{prefs->UserConfig(isLoggedIn=prefs[UserPrefsKeys.IS_LOGGED_IN]?:false,userId=prefs[UserPrefsKeys.USER_ID]?:"",themeMode=prefs[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:DataStore<Preferences>bypreferencesDataStore(name="user_prefs",produceMigrations={context->listOf(SharedPreferencesMigration(context=context,sharedPreferencesName="user_config",// 旧 SP 文件名// 可选:仅迁移指定 keykeysToMigrate=setOf("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 文件,建议分批迁移,或合并到单一 DataStore:

produceMigrations={context->listOf(SharedPreferencesMigration(context,"user_config"),SharedPreferencesMigration(context,"app_settings"),SharedPreferencesMigration(context,"feature_flags"))}

注意:多个迁移器按顺序执行,每个都是独立事务。

5.3 自定义 Key 映射(旧 Key → 新 Key)

旧 SP 使用不规范 key 名(如驼峰、带空格),迁移时可重命名:

SharedPreferencesMigration(context=context,sharedPreferencesName="user_config",migrate={spData,currentData->valmutablePrefs=currentData.toMutablePreferences()// 旧 key: "UserIsLoggedIn" → 新 key: "is_logged_in"spData.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){valoldPrefs=context.getSharedPreferences("user_config",Context.MODE_PRIVATE)valnewPrefs=context.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 注入 DataStore

@Module@InstallIn(SingletonComponent::class)objectDataStoreModule{@Provides@SingletonfunprovideUserPrefsDataStore(@ApplicationContextcontext:Context):DataStore<Preferences>=context.userPrefsDataStore}@HiltViewModelclassSettingsViewModel@Injectconstructor(privatevaluserPrefsRepo:UserPrefsRepository):ViewModel(){valthemeMode:StateFlow<Int>=userPrefsRepo.themeMode.stateIn(scope=viewModelScope,started=SharingStarted.WhileSubscribed(5_000),initialValue=0)funonThemeSelected(mode:Int){viewModelScope.launch{userPrefsRepo.setThemeMode(mode)}}}

6.2 在 Compose 中消费

@ComposablefunSettingsScreen(viewModel:SettingsViewModel=hiltViewModel()){valthemeModebyviewModel.themeMode.collectAsStateWithLifecycle()ThemeSelector(selected=themeMode,onSelect=viewModel::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()valprefs=runBlocking{userPrefsDataStore.data.first()}// 主线程 blockif(prefs[UserPrefsKeys.IS_LOGGED_IN]==true){...}}}

解决:将逻辑移到首个 Activity/Fragment 的协程中,或使用ApplicationCoroutineScope

classMyApp:Application(),CoroutineScopebyMainScope(){overridefunonCreate(){super.onCreate()launch{valprefs=userPrefsDataStore.data.first()// 异步处理}}}

坑 2:迁移后旧 SP 文件仍被某处代码访问

现象:用户数据在 DataStore 和 SP 之间出现不一致。
原因:代码中仍有旧的getSharedPreferences("user_config", ...)调用,绕过了 DataStore。
复现:迁移后的老代码路径(如 WebView Bridge、旧 Fragment)未同步更新。
解决:用 Lint 规则强制拦截 SP 调用:

// 在 lint.xml 中禁用 SharedPreferences 用法<issue id="CommitPrefEdits"severity="error"/>// 或自定义 Lint 规则检测 getSharedPreferences() 调用

坑 3:DataStore 文件损坏后无法恢复

现象:极少数情况下(存储空间不足、进程被 kill)导致 Proto 文件损坏,dataFlow 持续抛出异常。
原因:DataStore 的 CorruptionHandler 未配置,默认重抛异常。
解决:配置ReplaceFileCorruptionHandler

valContext.userPrefsDataStore:DataStore<Preferences>bypreferencesDataStore(name="user_prefs",corruptionHandler=ReplaceFileCorruptionHandler{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:StateFlow<Boolean>=repo.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 条记录更新单条耗时 <1ms;DataStore 全量写入同等数据需 20~50ms。


9. 总结

  1. SharedPreferences 的主线程 I/O 与非原子提交是 ANR 的根源,DataStore 从架构层面消除了这两个风险。
  2. preferencesDataStore委托属性是创建单例的唯一正确姿势,多实例会导致数据损坏。
  3. 内置SharedPreferencesMigration提供零代码迁移路径,支持 Key 重映射与数据过滤。
  4. DataStore 不支持跨进程,多进程场景需借助 ContentProvider 或改用 Room。
  5. 配置ReplaceFileCorruptionHandler是生产环境的必选项,防止文件损坏导致 App 不可用。

核心结论:DataStore 不是 SharedPreferences 的简单替换,而是对持久化 KV 存储的重新设计——将 I/O 安全性、类型安全、错误处理的责任从调用者转移到了框架本身。


参考资料

  • DataStore 官方文档
  • 从 SharedPreferences 迁移到 DataStore
  • DataStore 设计文档(Medium)
  • AOSP 源码:frameworks/base/core/java/android/app/SharedPreferencesImpl.java
  • AOSP 源码:frameworks/base/core/java/android/app/QueuedWork.java
  • DataStore 源码:androidx/datastore/core/SingleProcessDataStore.kt

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询