史上最全面,清晰的SharedPreferences解析

基础用法
获取Sp:
get
put
监听器
原理分析
获取SharedPreferences
构造SharedPreferences
getX原理分析
putX原理分析
创建editor
putString
apply
apply总结
commit
SharedPreferences最佳实践
勿存储过大value
勿存储复杂数据
不要乱edit和apply,尽量批量修改一次提交
建议apply,少用commit
registerOnSharedPreferenceChangeListener弱引用问题
apply和commit对registerOnSharedPreferenceChangeListener的影响
不要有任何用SP进行多进程存储的幻想
基础用法

获取Sp:

Activity中:getPreferences(int mode)
context.getSharedPreferences(String name, int mode)
PreferenceManager.getDefaultSharedPreferences(Context context)
1和3的获取SP的方法最终都会调用2,只是1和3默认选取了特定的name,1中通过getLocalClassName()获取通过包名和类名拼装的name,3通过context.getPackageName() + "_preferences"获取name
注意第二个参数的含义,现在均指定为MODE_PRIVATE,其余的都被废弃。含义如下:File creation mode: the default mode, where the created file can only be accessed by the calling application (or all applications sharing the same user ID).
所存储的数据保存在:/data/data/<package name>/shared_prefs下的指定name.xml文件中
get

sp.getX(String key, X value);
1
X为简单的基本类型:float,int,long,String,Boolean,Set
put

SharedPreferences sharedPreferences = getPreferences(0);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putFloat("float", 1f);
editor.putBoolean("boolean", true);
editor.apply();
1
2
3
4
5
首先获取Editor对象,操作完需要进行事务提交操作,可以采用commit或者apply进行。commit同步写磁盘,返回是否成功的标识码。apply异步写磁盘,无返回值。(二者均是同步写内存,先同步写内存,之后同步/异步写磁盘)
监听器

sharedPreferences.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {

}
});
1
2
3
4
5
6
如果使用匿名内部类的形式进行监听。注意,因为OnSharedPreferenceChangeListener的引用被保存在一个WeakHashMap中,导致程序的行为不确定性。为了避免这种情况,推荐以下方式:
private OnSharedPreferenceChangeListener mListener = new OnSharedPreferenceChangeListener() {

@Override
public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) {
Log.i(LOGTAG, "instance variable key=" + key);
}
};

@Override
protected void onResume() {
PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).registerOnSharedPreferenceChangeListener(mListener);
super.onResume();
}

@Override
protected void onPause() {
PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).unregisterOnSharedPreferenceChangeListener(mListener);
super.onPause();
原理分析

获取SharedPreferences

public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}

final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}

// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}

sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// If somebody else (some other process) changed the prefs
// file behind our back, we reload it. This has been the
// historical (if undocumented) behavior.
sp.startReloadIfChangedUnexpectedly();
}
return sp;

可见 sdk 是先取了缓存(sSharedPrefs静态变量), 如果缓存未命中, 才构造对象. 也就是说, 多次 getSharedPreferences 几乎是没有代价的. 同时, 实例的构造被 synchronized 关键字包裹, 因此构造过程是多线程安全的
构造SharedPreferences

第一次构建SharedPreferences对象
// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int www.huarenyl.cn mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk(www.mcyllpt.com);
几个关键类成员信息解释如下
1. mFile 代表我们磁盘上的配置文件
2. mBackupFile 是一个灾备文件, 用户写入失败时进行恢复, 后面会再说. 其路径是 mFile 加后缀 ‘.bak’
3. mMap 用于在内存中缓存我们的配置数据, 也就是 getXxx 数据的来源

重点关注startLoadFromDisk()方法

private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run(www.thd178.com ) {
loadFromDisk(www.taohuayuan178.com );
开启了一个从Disk读取的线程

// SharedPreferencesImpl.java
private void loadFromDisk(www.douniu157.com) {
synchronized (SharedPreferencesImpl.this) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}

... 略去无关代码 ...

str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);

synchronized (SharedPreferencesImpl.this) {
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();;
}
notifyAll();
loadFromDisk()非常关键,他总共做了以下几件事
1. 如果有 ‘灾备’ 文件, 则直接使用灾备文件回滚.
2. 把配置从磁盘读取到内存的并保存在 mMap 字段中(看代码最后 mMap = map)
3. 标记读取完成, 这个字段后面 awaitLoadedLocked 会用到. 记录读取文件的时间, 后面 MODE_MULTI_PROCESS 中会用到
4. 发一个 notifyAll 通知已经读取完毕, 激活所有等待加载的其他线程

这里写图片描述

getX原理分析

public float getFloat(String key, float defValue) {
synchronized (this) {
awaitLoadedLocked();
Float v = (Float)mMap.get(key);
return v != null ? v : defValue;
关键信息如下:
1. synchronized保证了线程安全
2. get操作一定是从mMap中读取,既从内存中读取,无过多性能损耗。
3. awaitLoadedLocked()保证了读取操作一定在loadFromDisk()执行之完,同步等待。因此第一次调用get操作可能会阻塞,万分注意,这也是sp被定义为轻量级存储系统的重要原因

putX原理分析

put操作较为复杂,一步一步分析

创建editor

// SharedPreferencesImpl.java
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (this) {
awaitLoadedLocked();
}


EditorImpl()无构造函数,仅仅去初始化两个成员变量

// SharedPreferencesImpl.java
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;

... 略去方法定义 ...
public Editor putString(String key, @Nullable String value) { ... }
public boolean commit() { ... }

关键信息如下:
1. ·mModified 是我们每次 putXxx 后所改变的配置项
2. mClear 标识要清空配置项

putString

public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
很简单, 仅仅是把我们设置的配置项放到了 mModified 属性里保存. 等到 apply 或者 commit 的时候回写到内存和磁盘. 咱们分别来看看

apply

// SharedPreferencesImpl.java
public void apply() {
final MemoryCommitResult mcr = commitToMemory();

... 略无关 ...

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
apply核心在于两点
1. commitToMemory()完成了内存的同步回写
2. enqueueDiskWrite() 完成了硬盘的异步回写, 我们接下来具体看看

// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {

... 略去无关 ...

mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;

synchronized (this) {
for (Map.Entry&lt;String, Object&gt; e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// &quot;this&quot; is the magic value for a removal mutation. In addition,
// setting a value to &quot;null&quot; for a given key is specified to be
// equivalent to calling remove on that key.
if (v == this || v == null) {
mMap.remove(k);
} else {
mMap.put(k, v);
}
}

mModified.clear();
}
}
return mcr;
两个关键信息
1. 把 Editor.mModified 中的配置项回写到 SharedPreferences.mMap 中, 完成了内存的同步
2. 把 SharedPreferences.mMap 保存在了 mcr.mapToWriteToDisk 中. 而后者就是即将要回写到磁盘的数据源

// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}

...
}
};

...

QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
关键信息
使用singleThreadExecutor单一线程池去依次执行写入磁盘的runnable序列

之后是真正执行把数据写入磁盘的方法
// SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
if (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
return;
}
} else {
mFile.delete();
}
}

// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
return;
}

// Clean up an unsuccessfully written file
mFile.delete();
主要分为三个过程:
1. 先把已存在的老的配置文件重命名(加 ‘.bak’ 后缀), 然后删除老的配置文件. 这相当于做了灾备
2. 向 mFile 中一次性写入所有配置项. 即 mcr.mapToWriteToDisk(这就是 commitToMemory 所说的保存了所有配置项的字段) 一次性写入到磁盘. 如果写入成功则删除灾备文件, 同时记录了这次同步的时间
3. 如果上述过程 [2] 失败, 则删除这个半成品的配置文件

apply总结

由于apply比较复杂,稍作总结:
1. 通过 commitToMemory 将修改的配置项同步回写到内存 SharedPreferences.mMap 中. 此时, 任何的 getXxx 都可以获取到最新数据了
2. 通过 enqueueDiskWrite 调用 writeToFile 将所有配置项一次性异步回写到磁盘. 这是一个单线程的线程池

这里写图片描述

commit

commit比较简单,直接看代码和时序图即可,大致和apply相同
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
这里写图片描述

注意:commit最后会等待异步任务返回,说明会阻塞当前调用线程,因此说commit是同步写入,apply是异步写入。

以上涵盖了大部分SharedPreferences重要源码分析,下面总结SharedPreferences最佳实践,提出日后要注意的问题,只说结论不解释原因。(如果你不明白为什么,证明你前面的分析没有深刻理解)
SharedPreferences最佳实践

勿存储过大value

永远记住,SharedPreferences是一个轻量级的存储系统,不要存过多且复杂的数据,这会带来以下的问题
第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
这些key和value会永远存在于内存之中,占用大量内存。
勿存储复杂数据

SharedPreferences通过xml存储解析,JSON或者HTML格式存放在sp里面的时候,需要转义,这样会带来很多&这种特殊符号,sp在解析碰到这个特殊符号的时候会进行特殊的处理,引发额外的字符串拼接以及函数调用开销。如果数据量大且复杂,严重时可能导频繁GC。
不要乱edit和apply,尽量批量修改一次提交

edit会创建editor对象,每进行一次apply就会创建线程,进行内存和磁盘的同步,千万写类似下面的代码
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
sp.edit().putString("test1", "sss").apply();
sp.edit().putString("test2", "sss").apply();
sp.edit().putString("test3", "sss").apply();
sp.edit().putString("test4", "sss").apply();
建议apply,少用commit

commit同步写内存,同步写磁盘。有是否成功的返回值
apply同步写内存,异步写磁盘。无返回值
registerOnSharedPreferenceChangeListener弱引用问题

见本文初
apply和commit对registerOnSharedPreferenceChangeListener的影响

对于 apply, listener 回调时内存已经完成同步, 但是异步磁盘任务不保证是否完成
对于 commit, listener 回调时内存和磁盘都已经同步完毕
不要有任何用SP进行多进程存储的幻想

这个话题不需要过多讨论,只记住一点,多进程别用SP,Android没有对SP在多进程上的表现做任何约束和保证。附上Google官方注释
@deprecated MODE_MULTI_PROCESS does not work reliably in
some versions of Android, and furthermore does not provide any mechanism for reconciling

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值