12、数据存储之SharedPreferences:轻量级数据存储、读写操作、登录状态保存实战

做Android开发这些年,我遇到过不少新手问:“我就想存个用户设置,比如开关状态、登录标记,有必要上SQLite吗?”

我的回答永远是:杀鸡别用牛刀。这种场景,SharedPreferences就是最趁手的工具。

什么是SharedPreferences?

说白了,它就是一个轻量级的键值对存储方案。底层用XML文件存数据,放在/data/data/包名/shared_prefs/目录下。

适合存什么?

  • 用户偏好设置(主题、字体大小)
  • 登录状态标记
  • 简单的计数器或标记位
  • 应用配置开关

不适合存什么?

  • 大量结构化数据(请用Room或SQLite)
  • 敏感信息(密码、Token——除非加密)
  • 文件或二进制数据

核心要点:SharedPreferences是Map结构,key为String,value支持:String、int、long、float、boolean、Set<String>。

基本读写操作

先看获取实例的方式。我个人习惯用getSharedPreferences(),因为可以指定文件名,方便模块化管理。

// 获取SharedPreferences实例
SharedPreferences sp = getSharedPreferences("user_config", Context.MODE_PRIVATE);
// 获取编辑器
SharedPreferences.Editor editor = sp.edit();

// 写入数据
editor.putString("username", "张三");
editor.putBoolean("is_login", true);
editor.putInt("age", 28);
editor.apply(); // 异步提交,推荐

// 读取数据
String username = sp.getString("username", "默认用户");
boolean isLogin = sp.getBoolean("is_login", false);
int age = sp.getInt("age", 0);

这里有个细节:apply()commit()有什么区别?

  • apply():异步写入,无返回值,速度快,推荐在主线程使用。
  • commit():同步写入,返回boolean表示成功与否,会阻塞主线程。

我在项目中遇到过因为用commit()导致界面卡顿的情况,后来全部换成了apply()。除非你需要在写入后立即判断结果,否则别用commit。

登录状态保存实战

这是SharedPreferences最经典的应用场景。我们来实现一个完整的登录状态管理。

public class LoginManager {
    private static final String SP_NAME = "login_sp";
    private static final String KEY_IS_LOGIN = "is_login";
    private static final String KEY_TOKEN = "token";
    private static final String KEY_USER_ID = "user_id";
    
    private SharedPreferences sp;
    
    public LoginManager(Context context) {
        sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
    }
    
    // 保存登录状态
    public void saveLoginState(String token, String userId) {
        sp.edit()
            .putBoolean(KEY_IS_LOGIN, true)
            .putString(KEY_TOKEN, token)
            .putString(KEY_USER_ID, userId)
            .apply();
    }
    
    // 检查是否登录
    public boolean isLogin() {
        return sp.getBoolean(KEY_IS_LOGIN, false);
    }
    
    // 获取Token
    public String getToken() {
        return sp.getString(KEY_TOKEN, "");
    }
    
    // 清除登录状态(退出登录)
    public void clearLoginState() {
        sp.edit()
            .putBoolean(KEY_IS_LOGIN, false)
            .remove(KEY_TOKEN)
            .remove(KEY_USER_ID)
            .apply();
    }
}

我的经验:退出登录时不要直接调用clear()清空整个SP文件。万一你在这个文件里还存了其他非登录相关的配置,就全没了。用remove()精准删除,更安全。

避坑指南

我曾经踩过一个坑:在多个进程同时读写同一个SharedPreferences文件,结果数据丢失了。

为什么会这样?因为SharedPreferences默认不是线程安全的,更不是进程安全的。

解决方案:

  • 如果只是单进程,放心用,没问题。
  • 如果多进程场景,可以改用Context.MODE_MULTI_PROCESS(但已废弃,不推荐)。
  • 更好的做法:使用ContentProviderMMKV(腾讯开源的高性能KV存储)。

警告:不要在SharedPreferences里存密码!明文存储太危险。如果非要存敏感信息,请使用EncryptedSharedPreferences(AndroidX Security库提供)。

知识体系结构图

下面这张图帮你理清SharedPreferences的核心脉络:

SharedPreferences getSharedPreferences() edit() → Editor getXxx() 直接读取 putXxx(key, value) 写入 apply() 异步推荐 commit() 同步慎用 登录状态 / 用户偏好 配置开关 / 简单标记

进阶技巧:监听数据变化

有时候我们需要在SharedPreferences数据变化时做响应。比如用户切换了主题,界面要立刻刷新。

SharedPreferences提供了registerOnSharedPreferenceChangeListener接口:

SharedPreferences.OnSharedPreferenceChangeListener listener = 
    new SharedPreferences.OnSharedPreferenceChangeListener() {
    @Override
    public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
        if ("theme_mode".equals(key)) {
            // 切换主题
            recreate();
        }
    }
};

sp.registerOnSharedPreferenceChangeListener(listener);

// 别忘了在不需要时取消注册
// sp.unregisterOnSharedPreferenceChangeListener(listener);

注意:这个回调是在主线程执行的。如果你的处理逻辑比较重,记得开子线程或使用Handler。

性能与最佳实践

SharedPreferences虽然轻量,但也不是无限制用的。我总结了几条经验:

  1. 单个SP文件不要太大——超过100个key就该考虑拆分文件了。
  2. 避免频繁写入——每次apply()都会触发一次文件IO,高频写入会卡UI。
  3. 善用默认值——读取时给个合理的默认值,避免空指针。
  4. 模块化存储——登录信息放一个文件,设置放一个文件,互不干扰。

嗯,SharedPreferences就是这么个简单又实用的东西。别小看它,用好了能让你的应用轻快又稳定。下次遇到“存个小数据”的需求,别犹豫,就是它了。


公众号:蓝海资料掘金营,微信deep3321