从SharedPreferences到DataStore:Android存储进化之路 从SharedPreferences到DataStoreAndroid存储进化之路SharedPreferences 的辉煌与落幕在早期的 Android 开发中SharedPreferences 可谓是开发者们的得力助手是数据存储领域的 “明星产品” 。它就像是一个轻量级的 “保险箱”以简单易用的键值对Key-Value形式帮助开发者轻松存储和获取各种基本数据类型如布尔值、整型、字符串等而且它还能自动将数据持久化到设备存储中方便应用在不同会话之间保留关键信息。凭借这种简单直接的使用方式SharedPreferences 迅速成为了 Android 开发中存储少量配置信息、用户偏好设置等场景的首选方案。比如保存用户的登录状态、主题设置、推送通知开关等它都能完美胜任。在那个时期的 Android 应用代码中到处都能看到 SharedPreferences 的身影它就像一位默默奉献的幕后英雄支撑着无数应用的正常运转为开发者们节省了大量的时间和精力成为了 Android 开发生态中不可或缺的一部分。SharedPreferences 的痛点大揭秘随着 Android 应用开发的不断演进对数据存储的性能、安全性和稳定性提出了更高要求。在这个过程中SharedPreferences 的一些局限性逐渐暴露出来曾经的辉煌逐渐被一系列痛点所掩盖。一阻塞主线程在初始化 SharedPreferences 时它会将整个文件内容加载到内存中这个过程是通过子线程进行 IO 读取并完成 XML 解析 。然而在解析完成之前其他所有操作如 getXXX () 和 edit ()都需要等待初始化完成。这就意味着如果在主线程中调用这些操作而此时初始化尚未完成主线程就会被阻塞从而影响应用的响应速度甚至可能导致 ANRApplication Not Responding错误。例如在应用启动时如果需要从 SharedPreferences 中读取大量配置信息而此时初始化过程较慢就会使应用的启动界面长时间处于空白状态给用户带来极差的体验。二类型安全缺失SharedPreferences 在存储和读取数据时不能保证类型安全。由于它使用相同的键Key进行操作putXXX 方法可以使用不同类型的数据覆盖掉相同的键 。这就导致在读取数据时如果使用了错误的类型方法如用 getString () 方法去读取一个原本存储为整型的数据就会出现 ClassCastException 异常。例如SharedPreferencesspgetSharedPreferences(test,Context.MODE_PRIVATE);sp.edit().putInt(key,1).apply();Stringvaluesp.getString(key,);// 这里会抛出ClassCastException异常三内存浪费严重一旦 SharedPreferences 加载了数据这些数据就会一直留在内存中。因为它通过静态的 ArrayMap 缓存每一个 SP 文件而每个 SP 文件内容又通过 Map 缓存键值对数据 。这对于一些内存资源有限的设备来说是一个不小的负担。尤其是当存储的数据量较大时会占用大量的内存空间导致应用的内存开销增大甚至可能引发内存泄漏影响应用的整体性能。四异步隐患重重虽然 apply () 方法被设计为异步提交数据以避免阻塞主线程但由于其设计问题仍然可能会导致程序发生 ANR 。当调用 apply () 方法时数据会先被原子提交到内存然后再异步提交到硬件磁盘。在这个过程中如果在主线程中调用了 QueuedWork.waitToFinish () 方法而此时异步提交任务尚未完成主线程就会被阻塞。特别是在数据量较大或者设备性能较低的情况下这种阻塞可能会持续较长时间从而引发 ANR。例如在应用切换页面时如果有大量的 SharedPreferences 异步提交任务尚未完成而此时系统调用了 QueuedWork.waitToFinish () 方法就可能导致页面切换卡顿甚至无响应。五数据一致性难保SharedPreferences 没有事务性 API这就意味着在并发访问或者出现异常的情况下无法保证数据的一致性 。例如当多个线程同时对 SharedPreferences 进行写入操作时可能会出现数据丢失或者数据错误的情况。因为它的写入操作不是原子性的可能会被其他线程的操作打断。另外在使用 apply () 方法异步提交数据时如果在数据还未完全写入磁盘之前应用进程被杀死那么这些未写入的数据就会丢失导致内存与磁盘数据不一致。DataStore 闪亮登场面对 SharedPreferences 的诸多痛点Google 推出了 DataStore作为其替代方案为开发者提供了更强大、更可靠的数据存储方式。DataStore 是 Jetpack 组件库中的一员它以其异步、类型安全和事务性的特性迅速成为了 Android 开发者们的新宠。一DataStore 初相识DataStore 是 Jetpack 组件库中的一部分专门用于在 Android 应用中存储简单的键值对数据或类型化对象 。它的出现旨在解决 SharedPreferences 存在的问题提供一种更现代化、更高效、更安全的数据存储解决方案。DataStore 基于 Kotlin 协程和 Flow 构建这使得它能够以异步、非阻塞的方式进行数据存储和读取操作从而避免了对主线程的阻塞保证了应用的流畅性 。同时它提供了类型安全的保障特别是在 Proto DataStore 中通过 Protobuf 定义数据结构能有效避免类型转换错误。而且DataStore 还具备事务性更新的特点确保数据的一致性和完整性从根本上解决了 SharedPreferences 在数据一致性方面的不足。二两种实现方式详解DataStore 提供了两种不同的实现方式以满足不同场景下的数据存储需求。Preferences DataStorePreferences DataStore 适用于存储简单的键值对数据它的使用方式与 SharedPreferences 类似但在性能和安全性上有了显著提升 。在使用 Preferences DataStore 时我们首先需要在项目的 build.gradle 文件中添加依赖implementationandroidx.datastore:datastore-preferences:1.1.1然后通过以下代码获取 Preferences DataStore 的实例valContext.dataStore:DataStorePreferencesbypreferencesDataStore(namesettings)接下来就可以进行数据的读写操作了。例如定义一个键并写入数据privatevalKEY_NAMEstringPreferencesKey(name)suspendfunsaveName(context:Context,name:String){context.dataStore.edit{preferences-preferences[KEY_NAME]name}}读取数据时可以使用 Flow 来监听数据的变化valnameFlow:FlowString?context.dataStore.data.map{preferences-preferences[KEY_NAME]}Proto DataStoreProto DataStore 则更适合存储复杂的结构化数据 。它基于 Protocol BuffersProtobuf技术通过定义数据结构的.proto 文件生成对应的 Java 或 Kotlin 类从而实现对复杂对象的高效存储和读取。在使用 Proto DataStore 时需要先定义.proto 文件例如syntax proto3; package com.example; message User { string name 1; int32 age 2; string email 3; }然后在项目的 build.gradle 文件中添加相关依赖并配置 Protobuf 插件生成对应的 Java 或 Kotlin 类 。接着实现一个 Serializer 来处理数据的读写objectUserSerializer:SerializerUser{overridevaldefaultValue:Userget()User.getDefaultInstance()overridesuspendfunreadFrom(input:InputStream):User{try{returnUser.parseFrom(input)}catch(e:IOException){throwCorruptionException(Cannot read proto.,e)}}overridesuspendfunwriteTo(t:User,output:OutputStream){t.writeTo(output)}}最后获取 Proto DataStore 的实例并进行数据操作valContext.userDataStore:DataStoreUserbydataStore(fileNameuser.pb,serializerUserSerializer)suspendfunsaveUser(context:Context,user:User){context.userDataStore.updateData{currentUser-currentUser.toBuilder().mergeFrom(user).build()}}valuserFlow:FlowUsercontext.userDataStore.data通过这两种实现方式DataStore 为开发者提供了更加灵活和强大的数据存储能力无论是简单的配置信息还是复杂的对象数据都能轻松应对。DataStore 优势尽显一异步操作保流畅DataStore 基于 Kotlin 协程和 Flow 构建所有的读写操作都是异步的这是它相较于 SharedPreferences 的一大显著优势 。在 SharedPreferences 中初始化时会将整个文件内容加载到内存且 getXXX () 方法是同步的若在主线程调用当数据量较大或初始化未完成时会阻塞主线程影响应用响应速度 。而 DataStore 的异步操作使得在读取和写入数据时主线程不会被阻塞可以继续处理其他任务从而保证了应用的流畅性。例如当应用需要在启动时读取大量的用户配置数据时使用 DataStore 的异步读取操作用户几乎不会察觉到数据加载的过程应用可以快速展示界面提升了用户体验。二类型安全有保障在类型安全方面DataStore尤其是 Proto DataStore通过使用 Protocol Buffers协议缓冲区来定义数据模型在编译时就能进行严格的类型检查 。这意味着在代码编写阶段如果出现类型不匹配的错误编译器会立即报错避免了在运行时才发现类型转换错误的情况。例如我们定义一个 User 的 Proto 文件syntax proto3; package com.example; message User { string name 1; int32 age 2; }然后生成对应的 Kotlin 类。在使用时通过 Proto DataStore 存储和读取 User 对象编译器会确保数据类型的一致性。如果尝试将一个不符合 User 数据结构的数据进行存储或读取编译将无法通过。而在 SharedPreferences 中由于使用相同的键进行不同类型数据的操作很容易出现类型转换异常如前面提到的用 getString () 方法读取原本存储为整型的数据。三数据一致性可靠DataStore 提供了事务性更新的特性确保数据的一致性 。在进行数据更新时DataStore 会将所有的更改作为一个原子操作进行提交要么全部成功要么全部失败。这就避免了在并发情况下由于部分数据更新成功而部分失败导致的数据不一致问题。例如在一个多线程的场景下多个线程同时对用户的账户信息进行更新包括余额、积分等。使用 DataStore这些更新操作会被视为一个事务只有当所有的更新都成功完成后数据才会被持久化到存储中。如果其中任何一个更新操作失败整个事务会回滚保证了数据的完整性和一致性。而 SharedPreferences 没有事务性 API在多线程并发访问时可能会出现数据丢失或错误的情况。四响应式编程更便捷DataStore 与 Kotlin 的 Flow 结合实现了响应式编程使得数据变化的监听和处理变得更加便捷 。通过 Flow我们可以轻松地观察 DataStore 中数据的变化并在数据发生改变时自动触发相应的操作比如更新 UI。例如在一个设置界面中用户可以修改应用的主题模式当用户保存设置后DataStore 中的主题模式数据会发生变化此时通过 Flow 监听这个变化界面可以立即更新为用户选择的新主题实现了数据与 UI 的实时同步。示例代码如下valthemeFlow:FlowStringcontext.dataStore.data.map{preferences-preferences[KEY_THEME]?:default_theme}themeFlow.collectLatest{theme-// 根据新的主题更新UIupdateUI(theme)}在这个例子中当 DataStore 中的主题数据发生变化时Flow 会发射新的数据collectLatest 会收集这个变化并调用 updateUI 方法来更新 UI实现了响应式的 UI 更新。迁移实战指南一迁移步骤解析添加依赖首先在项目的 build.gradle 文件中添加 DataStore 的依赖。如果使用 Preferences DataStore添加以下依赖implementationandroidx.datastore:datastore-preferences:1.1.1如果使用 Proto DataStore除了上述依赖外还需要添加 protobuf 相关的依赖和插件配置 plugins{idcom.android.applicationidkotlin-androididcom.google.protobuf}protobuf{protoc{artifactcom.google.protobuf:protoc:3.19.4}generateProtoTasks{all().each{task-task.builtins{java{optionlite}}}}}dependencies{implementationandroidx.datastore:datastore:1.1.1implementationandroidx.datastore:datastore-proto:1.1.1implementationcom.google.protobuf:protobuf-java-util:3.19.4}创建 DataStore 实例对于 Preferences DataStore可以在 Context 扩展属性中创建实例 valContext.dataStore:DataStorePreferencesbypreferencesDataStore(namesettings)对于 Proto DataStore需要先定义.proto 文件然后创建 DataStore 实例 valContext.userDataStore:DataStoreUserbydataStore(fileNameuser.pb,serializerUserSerializer)迁移数据读取 SharedPreferences 中的数据并将其写入 DataStore 。例如迁移一个字符串类型的数据// 读取SharedPreferences数据valsharedPreferencesgetSharedPreferences(your_prefs_name,Context.MODE_PRIVATE)valspValuesharedPreferences.getString(your_key,default_value)// 写入DataStoresuspendfunmigrateData(){valkeystringPreferencesKey(your_key)dataStore.edit{settings-settings[key]spValue?:default_value}}验证迁移结果通过读取 DataStore 中的数据验证迁移是否成功 valnewValueFlow:FlowStringdataStore.data.map{preferences-preferences[stringPreferencesKey(your_key)]?:default_value}newValueFlow.collect{value-println(Migrated value:$value)}二注意事项提醒线程安全DataStore 的操作是异步的在使用时需要注意线程安全。特别是在多线程环境下避免对 DataStore 进行并发的写入操作以免出现数据不一致的问题。如果需要在多线程中使用 DataStore可以考虑使用协程的同步机制如 Mutex 来保证操作的原子性。数据类型转换在迁移数据时要注意数据类型的转换。虽然 DataStore 提供了类型安全的保障但在从 SharedPreferences 迁移数据时可能需要手动进行类型转换。例如如果在 SharedPreferences 中存储的是整型数据而在 DataStore 中使用的是字符串类型来接收就需要进行适当的转换。异常处理在进行数据迁移和操作 DataStore 时要做好异常处理。例如在读取或写入 DataStore 时可能会出现 I/O 异常、数据解析异常等。可以在代码中使用 try - catch 块来捕获异常并进行相应的处理如提示用户操作失败、记录日志等 。例如try{// 操作DataStore的代码}catch(e:IOException){// 处理I/O异常Log.e(DataStore,An I/O error occurred:${e.message})Toast.makeText(context,数据操作失败请稍后重试,Toast.LENGTH_SHORT).show()}catch(e:CorruptionException){// 处理数据损坏异常Log.e(DataStore,Data corruption occurred:${e.message})Toast.makeText(context,数据损坏请联系管理员,Toast.LENGTH_SHORT).show()}版本兼容性在添加 DataStore 依赖时要注意版本兼容性。不同版本的 DataStore 可能会有不同的特性和 API 变化确保所使用的版本与项目中的其他依赖库和 Android 系统版本兼容避免因版本冲突导致的编译错误或运行时异常 。在更新 DataStore 版本时仔细阅读官方文档中的版本变更说明及时调整代码以适应新的 API。