SharedPreferences(简称 SP)是 Android 开发中最资深的轻量级存储方案。虽然在 2026 年我们有很多现代化的替代品(如 DataStore、MMKV),但理解 SP 的原理依然是每个 Android 开发者打好地基的必修课。

一、 核心定义

SP 是一种基于 Key-Value(键值对) 的持久化机制。它底层通过 XML 文件 存储数据,并提供内存缓存以提高读取效率。

  • 存储路径: /data/data/<package_name>/shared_prefs/xxx.xml

  • 支持类型: boolean, int, float, long, String, Set<String>

二、基本使用

获取SharedPreferences对象

要想使用SharedPreferences来存储数据,首先需要获取到SharedPreferences 对象。Android中主要提供了2种方法用于得到SharedPreferences 对象。

Context 类中的getSharedPreferences()方法

此方法接收两个参数:

  • 第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data/<package name>/shared_prefs/目录下的。

  • 第二个参数用于指定操作模式,目前只有MODE_PRIVATE这一种模式可选,它是默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。

Activity 类中的getPreferences()方法

这个方法和Context中的getSharedPreferences() 方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为SharedPreferences的文件名。

注意:自API级别17以来,已弃用MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE模式。如果使用则抛出SecurityException。

三、 工作原理:内存与磁盘的“双重奏”

SP 的设计思路可以总结为:“一次加载,全程常驻,异步落盘”

  1. 加载阶段: 当你调用 getSharedPreferences() 时,系统会启动一个子线程去读取对应的 XML 文件,并将其内容反序列化到一个 Map 中。

  2. 读取阶段: 所有的 getXXX() 操作都是直接从内存中的 Map 读取的。这也是为什么 SP 读取速度很快的原因。

  3. 写入阶段: 通过 Editor 对象进行修改。修改会先更新内存中的 Map,然后再根据指令决定何时同步到磁盘文件。

四、 关键操作:Commit vs Apply

这是面试中的高频考点。理解这两者的区别是避坑的关键:

  • commit:直接在主线程中进行写入操作,属于同步提交,返回boolean值。容易阻塞主线程导致ANR。

  • apply:可能会导致数据丢失,将文件写入操作放到一个Runnable对象中,等待系统在工作线程中调用,属于异步提交,返回void,可能会导致数据丢失。其原理是创建一个等待锁放到QueuedlMork()中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。而Activity onPuase,onStop以及Scrvice处理 onStop,onStartCommand等情况下,就会执行QueuedWork.waitToFinish()等待QueuedlMork()中的所有问题执行完(主线程等待所有任务)。因此apply 调用次数过多也会容易引起ANR问题

特性

commit()

apply()

执行方式

同步

异步

返回值

boolean (成功/失败)

void (无返回值)

原子性

原子操作(同步写入文件)

原子操作(先改内存,再排队改文件)

主线程风险

(直接阻塞主线程直到 I/O 完成)

(虽是异步,但在生命周期结束时可能导致 ANR)

五、为什么被“嫌弃”?

  • 多进程不友好:MODE_MULTI_PROCESS 也没用。跨进程频繁读写可能导致数据损坏或丢失。

  • 全量加载:初始化时,SP 必须把整个 XML 文件一次性读完。如果你只想读一个 is_first_launch,但文件里存了 2MB 的杂物,你的主线程必须等着这 2MB 数据读取并解析完毕,导致明显的卡顿,也不宜存放大数据

  • 写文件也是全量: 即便你只改了一个布尔值,SP 也会把内存里完整的 Map 重新生成一份 XML 覆盖掉老文件。这种 I/O 开销随着数据量增大而呈指数级增长。

  • ANR风险高:无论是 commit() 还是 apply() ,可能造成卡顿或者 ANR。

  • 缺乏事务支持: 不支持像数据库那样的局部回滚或复杂的逻辑事务。

六、 最佳实践:如何正确使用 SP

如果你的项目中依然在使用 SP,请务必遵守以下“保命指南”:

  • 瘦身运动: 一个 SP 文件建议只存储几十个简单的配置项。严禁存入长文本、Base64 图片、复杂的 JSON 字符串。

  • 按需拆分: 不要把所有东西都塞进 config.xml。可以根据业务拆分成 user_settings.xml, palyer_config.xml 等,减少单文件加载压力。

  • 不要在主线程读写: 虽然读取走内存,但初始化读取文件是异步的,如果初始化没完成你就调用 get(),主线程会陷入等待。

  • 考虑迁移: 如果数据量超过 1KB 或者有高频写入需求,直接换成 MMKVDataStore

七、操作模式(Operating Modes)

1. 核心模式对比表

模式常量

状态

说明

MODE_PRIVATE

推荐/默认

私有模式。该文件只能被创建它的应用(或具有相同 UID 的应用)访问。这是目前唯一安全的标准模式。

MODE_WORLD_READABLE

已弃用 & 禁用

允许其他应用读取。从 API 17 开始弃用,从 API 24 (Android 7.0) 开始,如果传入此模式将直接抛出 SecurityException

MODE_WORLD_WRITEABLE

已弃用 & 禁用

允许其他应用写入。同样在 API 17 弃用,API 24 起触发崩溃。

MODE_MULTI_PROCESS

已弃用

旨在支持多进程共享 SP 数据。但由于底层无法保证原子性,极其容易丢失数据,已从 API 23 开始弃用。


2. 为什么 WORLD_READABLE/WRITEABLE 必须死?

在 Android 的早期,跨应用共享数据就像“西部荒野”一样自由,但这带来了巨大的安全隐患:

  • 隐私泄露: 任何 App 只要猜到你的包名,就能读取你的 Token、用户信息。

  • 数据污染: 恶意 App 可以随意篡改你的配置信息,导致你的应用逻辑崩溃。

现在的替代方案:

如果真的需要跨应用共享数据,Android 官方强制要求使用 FileProviderContentProvider。它们提供了基于 URI 的临时授权机制,比直接开文件权限要安全得多。


3. MODE_MULTI_PROCESS 的“谎言”

很多开发者在做多进程(比如有单独的 :remote 服务进程)时会尝试使用这个模式。

  • 真相: 设置了这个模式,系统只是在每次 getSharedPreferences 时重新从磁盘读取一次文件,而不是只读内存缓存。

  • 风险: 它依然无法解决并发写入的问题。两个进程同时 commit(),后写的会覆盖先写的,没有任何锁机制保证数据完整性。

  • 解决建议: 凡是涉及多进程 KV 存储,请直接放弃 SP,改用 MMKV


4. 正确的初始化姿势

即便 MODE_PRIVATE 的值是 0,为了代码的可读性和严谨性,建议始终显式传入:

Kotlin

// 2026 年的标准写法
val sp = context.getSharedPreferences("secure_settings", Context.MODE_PRIVATE)

如果你在 API 24 及以上设备上强行使用 MODE_WORLD_READABLE

Java

// 这行代码在现代设备上运行会直接让 App 崩溃
context.getSharedPreferences("danger_zone", Context.MODE_WORLD_READABLE); 
// 报错:java.lang.SecurityException: MODE_WORLD_READABLE no longer supported

总结建议

操作模式的收紧标志着 Android 从“开放获取”走向了“沙盒化管理”。

  • 单进程应用: 锁死 MODE_PRIVATE

  • 跨应用/跨进程: 放弃 SP 的模式逻辑,拥抱 ContentProvider 或支持多进程的存储框架(如 MMKV)。