基础用法
获取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

史上最全面,清晰的SharedPreferences解析的更多相关文章

  1. 史上最全HashMap红黑树解析

    HashMap红黑树解析 红黑树介绍 TreeNode结构 树化的过程 红黑树的左旋和右旋 TreeNode的左旋和右旋 红黑树的插入 TreeNode的插入 红黑树的删除 TreeNode的删除节点 ...

  2. 史上最全的CSP2019复习指南

    CSP2019复习指南 知识点(大纲)内容参考于本人博客: 近22年NOIP考点一览 算法 基本算法: 模拟.暴力枚举.排序.贪心.递归.递推.贪心.二分.位运算 这些算法不再在此加以赘述,如有考前还 ...

  3. 深入理解JavaScript系列:史上最清晰的JavaScript的原型讲解

    一说起JavaScript就要谈的几个问题,原型就是其中的一个.说了句大话,史上最清晰.本来是想按照大纲式的行文写一下,但写到后边感觉其实就一个概念,没有什么条理性,所以下面就简单按照概念解释的模式谈 ...

  4. 史上最最靠谱,又双叒叒简单的基于MSXML的XML解析指南-C++

    目录 史上最最靠谱,又双叒叒简单的基于MSXML的XML解析指南 流程设计 xml信息有哪几种读取形式(xml文件或wchar) 如何选取节点,and取节点属性有哪些方法? IXMLDOMNode与I ...

  5. Feign Ribbon Hystrix 三者关系 | 史上最全, 深度解析

    史上最全: Feign Ribbon Hystrix 三者关系 | 深度解析 疯狂创客圈 Java 分布式聊天室[ 亿级流量]实战系列之 -25[ 博客园 总入口 ] 前言 疯狂创客圈(笔者尼恩创建的 ...

  6. 史上最全的各种C++ STL容器全解析

    史上最全的C++ STL 容器大礼包 为什么\(C++\)比\(C\)更受人欢迎呢?除了\(C++\) 的编译令人感到更舒适,\(C++\)的标准模板库(\(STL\))也占了很重要的原因.当你还在用 ...

  7. GitHub上史上最全的Android开源项目分类汇总 (转)

    GitHub上史上最全的Android开源项目分类汇总 标签: github android 开源 | 发表时间:2014-11-23 23:00 | 作者:u013149325 分享到: 出处:ht ...

  8. 史上最全的iOS面试题及答案

    迷途的羔羊--专为路痴量身打造的品牌.史上最精准的定位.想迷路都难!闪电更新中...敬请期待,欢迎提意见.下载地址:https://itunes.apple.com/us/app/mi-tu-de-g ...

  9. 移动端IM开发者必读(二):史上最全移动弱网络优化方法总结

    1.前言 本文接上篇<移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”>,关于移动网络的主要特性,在上篇中已进行过详细地阐述,本文将针对上篇中提到的特性,结合我们的实践经 ...

随机推荐

  1. [转]理解Linux文件系统之inode

    很少转发别人的文章,但是这篇写的太好了. 理解inode   作者: 阮一峰 inode是一个重要概念,是理解Unix/Linux文件系统和硬盘储存的基础. 我觉得,理解inode,不仅有助于提高系统 ...

  2. Android线程管理(一)——线程通信

    线程通信.ActivityThread及Thread类是理解Android线程管理的关键. 线程,作为CPU调度资源的基本单位,在Android等针对嵌入式设备的操作系统中,有着非常重要和基础的作用. ...

  3. Python3中IO文件操作的常见用法

    首先创建一个文件操作对象: f = open(file, mode, encoding) file指定文件的路径,可以是绝对路径,也可以是相对路径 文件的常见mode: mode = “r”   # ...

  4. TensorFlow深度学习实战---图像数据处理

    图像的亮度.对比度等属性对图像的影响非常大,这些因素都会影响最后的识别结构.当然,复杂的预处理过程可能会导致训练效率的下降(利用TensorFlow中多线程处理输入数据的解决方案). 同一不同的原始数 ...

  5. vs2010(vs2012)好用的扩展插件介绍

    一直以来只使用番茄vs助手(https://www.wholetomato.com/downloads/default.asp)辅助写代码,也都忘了是谁介绍的,不过确实好用. 相比原始的vs,它提供了 ...

  6. 2019CSUST集训队选拔赛题解(三)

    PY学长的放毒题 Description 下面开始PY的香港之行,PY有n个要去的小吃店,这n个小吃店被m条路径联通起来. PY有1个传送石和n−1个传送石碎片. PY可以用传送石标记一个小吃店作为根 ...

  7. tensorflow中使用mnist数据集训练全连接神经网络-学习笔记

    tensorflow中使用mnist数据集训练全连接神经网络 ——学习曹健老师“人工智能实践:tensorflow笔记”的学习笔记, 感谢曹老师 前期准备:mnist数据集下载,并存入data目录: ...

  8. 算法笔记(c++)--完全背包问题

    算法笔记(c++)--完全背包和多重背包问题 完全背包 完全背包不同于01背包-完全背包里面的东西数量无限 假设现在有5种物品重量为5,4,3,2,1  价值为1,2,3,4,5  背包容量为10 # ...

  9. 联邦快递 IE和IP的区别 Fedex IE VS Fedex IP

    什么是FedEx IP? FedEx IP指的是联邦快递优先服务,时效比较快些,相对来说价格也比普通的高一些. 什么是FedEx IE? FedEx IE指的是联邦快递经济服务,时效与FedEx IP ...

  10. python序列成员资格

    可以用做登录操作,判断用户名密码是否正确! 代码示例: database = [ ['], ['], ['], ['] ] username = input("UserName: " ...