第 22 章JAVA30 分钟
数据存储:SharedPreferences
掌握 Android 轻量 KV 存储 SharedPreferences 的标准用法、用 Gson 持久化复杂对象、线程安全与封装工具类,并理解向 Jetpack DataStore 迁移的思路。
学习目标
- 理解 SharedPreferences 的基本模型与 apply/commit 差异
- 能够封装一个线程安全、类型友好的工具类
- 学会用 Gson 存储复杂对象
- 掌握 SharedPreferences 的线程模型与注意事项
- 了解 Jetpack DataStore 的定位与迁移方向
学习目标
- 理解 SharedPreferences 的存储模型与
apply/commit差异。 - 能够封装一个线程安全、类型友好的 SharedPreferences 工具类。
- 学会用 Gson 把复杂对象序列化为 JSON 持久化。
- 掌握 SharedPreferences 的线程模型与常见陷阱。
- 了解 Jetpack DataStore 的定位,以及向其迁移的方向。
SharedPreferences 数据存储实战
1. 基本概念
SharedPreferences(简称 SP)是 Android 提供的轻量 KV 存储方案:
- 文件存放在
/data/data/<package>/shared_prefs/<name>.xml,根节点为<map>,每个键值对是一个子标签。 - 支持
String、Set<String>、int、long、float、boolean五种基本类型。 - 读取即时返回内存缓存;写入通过
Editor提交。 - 同一进程内的实例会被缓存,跨进程使用需小心数据不一致。
2. 获取实例与读写
// 私有模式,仅本应用可访问;Context.MODE_PRIVATE 是当前推荐值
SharedPreferences sp = getSharedPreferences("user_prefs", Context.MODE_PRIVATE);
// 写
SharedPreferences.Editor editor = sp.edit();
editor.putString("token", "abcd1234");
editor.putInt("user_id", 1024);
editor.putBoolean("dark_mode", true);
editor.apply(); // 异步落盘,不阻塞 UI
// editor.commit(); // 同步阻塞返回,仅在必须立即确认落盘时使用
// 读
String token = sp.getString("token", null);
int userId = sp.getInt("user_id", -1);
boolean dark = sp.getBoolean("dark_mode", false);
// 删除某个 key
sp.edit().remove("token").apply();
// 清空全部
sp.edit().clear().apply();
apply vs commit:
| 维度 | apply() | commit() |
|---|---|---|
| 同步/异步 | 异步(先写内存,再异步落盘) | 同步阻塞 |
| 返回值 | 无 | boolean 是否成功 |
| 性能 | UI 线程安全,推荐默认用 | 频繁调用会卡顿 |
| 适用场景 | 99% 场景 | 必须立即落盘(如关键开关切换前) |
| 失败回调 | 无 | 通过返回值 |
3. 存储复杂对象:Gson 序列化
SP 只能存基本类型,复杂对象需先转成 JSON 字符串再存。
public class Profile {
public String nickname;
public int age;
public List<String> tags;
public boolean vip;
}
private static final Gson GSON = new Gson();
public void saveProfile(Profile profile) {
String json = GSON.toJson(profile);
getSharedPreferences("user_prefs", MODE_PRIVATE)
.edit()
.putString("profile_json", json)
.apply();
}
public Profile loadProfile() {
String json = getSharedPreferences("user_prefs", MODE_PRIVATE)
.getString("profile_json", null);
if (json == null) return null;
try {
return GSON.fromJson(json, Profile.class);
} catch (JsonSyntaxException e) {
return null; // 容错:版本升级导致结构变化时返回 null
}
}
注意:保存
Set<String>时不要直接修改原 Set 内容,应传入新 Set,否则 SP 内部缓存与磁盘不一致。
4. 封装一个线程安全的工具类
package com.example.storage;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import java.util.Set;
public class SpHelper {
private static final String DEFAULT_NAME = "app_prefs";
private static volatile SpHelper sInstance;
private final SharedPreferences sp;
private final Gson gson = new Gson();
private SpHelper(Context context, String name) {
sp = context.getApplicationContext()
.getSharedPreferences(name, Context.MODE_PRIVATE);
}
public static SpHelper get(Context context) {
return get(context, DEFAULT_NAME);
}
public static SpHelper get(Context context, String name) {
if (sInstance == null) {
synchronized (SpHelper.class) {
if (sInstance == null) {
sInstance = new SpHelper(context, name);
}
}
}
return sInstance;
}
// 基本类型读写
public void putString(String key, String value) {
sp.edit().putString(key, value).apply();
}
public String getString(String key, String def) {
return sp.getString(key, def);
}
public void putInt(String key, int value) { sp.edit().putInt(key, value).apply(); }
public int getInt(String key, int def) { return sp.getInt(key, def); }
public void putBoolean(String key, boolean value) {
sp.edit().putBoolean(key, value).apply();
}
public boolean getBoolean(String key, boolean def) {
return sp.getBoolean(key, def);
}
public void putLong(String key, long value) { sp.edit().putLong(key, value).apply(); }
public long getLong(String key, long def) { return sp.getLong(key, def); }
public void remove(String key) { sp.edit().remove(key).apply(); }
public void clear() { sp.edit().clear().apply(); }
// 任意对象读写(Gson 序列化)
public <T> void putObject(String key, T data) {
sp.edit().putString(key, data == null ? null : gson.toJson(data)).apply();
}
public <T> T getObject(String key, Class<T> clazz) {
return getObject(key, clazz, null);
}
public <T> T getObject(String key, Class<T> clazz, T def) {
String json = sp.getString(key, null);
if (TextUtils.isEmpty(json)) return def;
try {
return gson.fromJson(json, clazz);
} catch (JsonSyntaxException e) {
return def;
}
}
// 泛型集合读取:必须用 TypeToken
public <T> java.util.List<T> getList(String key, Class<T> clazz) {
String json = sp.getString(key, null);
if (TextUtils.isEmpty(json)) return null;
try {
java.lang.reflect.Type type = TypeToken.getParameterized(
java.util.List.class, clazz).getType();
return gson.fromJson(json, type);
} catch (JsonSyntaxException e) {
return null;
}
}
}
调用示例:
SpHelper.get(this).putString("token", "abc");
String token = SpHelper.get(this).getString("token", "");
Profile profile = new Profile();
profile.nickname = "Tom";
SpHelper.get(this).putObject("profile", profile);
Profile restored = SpHelper.get(this).getObject("profile", Profile.class);
List<String> tags = SpHelper.get(this).getList("tags", String.class);
5. 线程模型与线程安全
SharedPreferences读写线程安全:底层用synchronized保护HashMap内存缓存。- 读取是同步阻塞:首次
getSharedPreferences会从磁盘读取 XML 到内存,应在子线程或冷启动后台完成,避免影响启动帧。 apply是异步的:先写内存缓存(同步),后异步写盘;但apply之后立即读同进程数据是最新值。commit是同步阻塞:在主线程调用会卡顿;多次连续commit还会触发多次磁盘 IO。- 批量写入:连续多次
putXxx().apply()会触发多次磁盘写入;应合并到同一个Editor:
// 不推荐:3 次磁盘写入
sp.edit().putString("a", "1").apply();
sp.edit().putString("b", "2").apply();
sp.edit().putString("c", "3").apply();
// 推荐:1 次磁盘写入
SharedPreferences.Editor editor = sp.edit();
editor.putString("a", "1");
editor.putString("b", "2");
editor.putString("c", "3");
editor.apply();
- 跨进程不安全:SP 是单进程缓存模型,多进程场景下
MODE_MULTI_PROCESS已废弃,应改用ContentProvider、DataStore或文件锁方案。
6. 监听变化:OnSharedPreferenceChangeListener
SharedPreferences.OnSharedPreferenceChangeListener listener = (sp, key) -> {
if ("dark_mode".equals(key)) {
applyDarkMode(sp.getBoolean("dark_mode", false));
}
};
sp.registerOnSharedPreferenceChangeListener(listener);
// 销毁时注销,避免内存泄漏(SP 持有 Listener 强引用)
@Override
protected void onDestroy() {
sp.unregisterOnSharedPreferenceChangeListener(listener);
super.onDestroy();
}
注意:该监听只触发同进程内的写入;Listener 在 SP 内部以弱引用持有,必须用成员变量保存强引用,否则容易被 GC。
7. 注意事项与最佳实践
- 避免存大对象:单个值不要超过 100KB,否则文件 IO 影响启动;图片、长列表等大对象应走 Room 或文件存储。
- 避免存高频写入的数据:SP 不适合做计数器或时间戳,频繁 apply 会反复解析整份 XML。可考虑
LiveData + 内存缓存 + 周期性 apply。 - 避免存安全敏感信息:SP 文件以 XML 明文存储,root 设备可直接读取。Token、密码等应走
EncryptedSharedPreferences(Jetpack Security 库)。 MODE_WORLD_READABLE / MODE_WORLD_WRITEABLE已废弃:API 24+ 触发SecurityException,跨应用共享请用FileProvider或ContentProvider。- 不要把 SP 当数据库:它没有事务、查询、索引能力,复杂关系型数据请用 Room。
8. Jetpack DataStore 概览与迁移
Google 推出 Jetpack DataStore 作为 SP 的替代方案,定位是「类型安全、协程友好、事务性」的 KV 存储:
- Preferences DataStore:无 schema,KV 风格,最接近 SP 用法。
- Proto DataStore:基于 Protocol Buffers,类型安全,需定义 schema。
Java 项目可用 RxJava 风格的 DataStore(rxDataStore):
implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1"
implementation "androidx.datastore:datastore-rxjava3:1.1.1"
DataStore 关键差异:
| 维度 | SharedPreferences | DataStore |
|---|---|---|
| 异步模型 | 异步 apply,无错误回调 | 协程/RxJava 流式,支持错误回调 |
| 事务 | 无(apply 即一次性) | 支持原子事务 |
| 跨进程 | 不支持 | 通过 DataStore 单实例 + 锁可支持 |
| 类型安全 | 弱 | Proto 强类型,Preferences 通过 Key 包装 |
| 迁移 | — | 内置 SharedPreferencesMigration |
迁移思路:
- 创建 DataStore 实例并指定
migrations = listOf(SharedPreferencesMigration(...))。 - 旧 Key 第一次访问时自动从 SP 文件读出并写入 DataStore,之后 SP 文件可删除。
- 团队 Kotlin 化后,DataStore 与协程配合可显著简化异步流程。
实践建议:存量项目继续用 SP 不必强行迁移;新模块优先用 DataStore;Java 项目可用 RxJava 风格 DataStore 过渡。
常见坑与最佳实践
apply不会立即落盘:App 进程被强杀时可能丢最后一次写入,关键状态切换前用commit。registerOnSharedPreferenceChangeListener必须持有强引用:SP 内部用弱引用,被 GC 后监听失效。Set<String>是引用快照:先getStringSet再add修改不会触发监听,必须putStringSet新 Set。- 跨进程数据丢失:不要用 SP 做多进程同步,否则会偶发读到旧值。
- 存储
Set<String>顺序不保证:底层是HashSet,有序场景请改成List<String>+ Gson。 - 大对象 SharedPreferences 严重影响启动:首次读取会加载全部 XML 到内存,单文件 > 1MB 时务必迁移到 Room。
MODE_MULTI_PROCESS已废弃且不可靠:跨进程请用 ContentProvider 或 DataStore。- 加密敏感数据:API Key、Token 用
EncryptedSharedPreferences或自己 AES 加密后再写入。
章节小结
- SharedPreferences 适合轻量、低频写入的 KV 存储,
apply默认使用,commit仅在必须立即落盘时用。 - 复杂对象先 Gson 序列化为 JSON 再写入,反序列化务必容错。
- 工具类封装可统一线程安全、容错、类型推断;批量写入合并到同一 Editor。
- SharedPreferences 不适合高频、跨进程、大对象、敏感数据场景,应迁移到 DataStore / Room / EncryptedSharedPreferences。
下一章预告
当数据量大、关系复杂时,KV 存储已不够用。下一章《数据库:Room 完整实战》将系统讲解 Room 三层架构、注解、关系查询、迁移、与 LiveData/RxJava 集成的完整方案。