A
第 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. 注意事项与最佳实践

  1. 避免存大对象:单个值不要超过 100KB,否则文件 IO 影响启动;图片、长列表等大对象应走 Room 或文件存储。
  2. 避免存高频写入的数据:SP 不适合做计数器或时间戳,频繁 apply 会反复解析整份 XML。可考虑 LiveData + 内存缓存 + 周期性 apply。
  3. 避免存安全敏感信息:SP 文件以 XML 明文存储,root 设备可直接读取。Token、密码等应走 EncryptedSharedPreferences(Jetpack Security 库)。
  4. MODE_WORLD_READABLE / MODE_WORLD_WRITEABLE 已废弃:API 24+ 触发 SecurityException,跨应用共享请用 FileProvider 或 ContentProvider。
  5. 不要把 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

迁移思路:

  1. 创建 DataStore 实例并指定 migrations = listOf(SharedPreferencesMigration(...))。
  2. 旧 Key 第一次访问时自动从 SP 文件读出并写入 DataStore,之后 SP 文件可删除。
  3. 团队 Kotlin 化后,DataStore 与协程配合可显著简化异步流程。

实践建议:存量项目继续用 SP 不必强行迁移;新模块优先用 DataStore;Java 项目可用 RxJava 风格 DataStore 过渡。

常见坑与最佳实践

  1. apply 不会立即落盘:App 进程被强杀时可能丢最后一次写入,关键状态切换前用 commit。
  2. registerOnSharedPreferenceChangeListener 必须持有强引用:SP 内部用弱引用,被 GC 后监听失效。
  3. Set<String> 是引用快照:先 getStringSet 再 add 修改不会触发监听,必须 putStringSet 新 Set。
  4. 跨进程数据丢失:不要用 SP 做多进程同步,否则会偶发读到旧值。
  5. 存储 Set<String> 顺序不保证:底层是 HashSet,有序场景请改成 List<String> + Gson。
  6. 大对象 SharedPreferences 严重影响启动:首次读取会加载全部 XML 到内存,单文件 > 1MB 时务必迁移到 Room。
  7. MODE_MULTI_PROCESS 已废弃且不可靠:跨进程请用 ContentProvider 或 DataStore。
  8. 加密敏感数据:API Key、Token 用 EncryptedSharedPreferences 或自己 AES 加密后再写入。

章节小结

  • SharedPreferences 适合轻量、低频写入的 KV 存储,apply 默认使用,commit 仅在必须立即落盘时用。
  • 复杂对象先 Gson 序列化为 JSON 再写入,反序列化务必容错。
  • 工具类封装可统一线程安全、容错、类型推断;批量写入合并到同一 Editor。
  • SharedPreferences 不适合高频、跨进程、大对象、敏感数据场景,应迁移到 DataStore / Room / EncryptedSharedPreferences。

下一章预告

当数据量大、关系复杂时,KV 存储已不够用。下一章《数据库:Room 完整实战》将系统讲解 Room 三层架构、注解、关系查询、迁移、与 LiveData/RxJava 集成的完整方案。