基础用法
获取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. springboot+security+JWT实现单点登录

    本次整合实现的目标:1.SSO单点登录2.基于角色和spring security注解的权限控制. 整合过程如下: 1.使用maven构建项目,加入先关依赖,pom.xml如下: <?xml v ...

  2. mysql 创建外键时发生错误的原因和解决方法

    可以去网上查看错误号,就能知道到底哪里出错了 https://zhidao.baidu.com/question/359868536.html 这里1452对应的错误是因为建立外键的表中还有数据,所以 ...

  3. python2 - 列表

    列表 a = [1,2,3,4,5,6,7] a[0:4:1]//正向索引 a[-1:-2:-1]//反向索引 列表添加 a = [1, 2] b = [3, 4] +:a + b//把a和b连接,重 ...

  4. MySQL☞大结局

    emmm,看了这么多大概会用了点点,学到了一点点 select  列名/*/聚合函数 from  表名1 别名1  连接查询(左外.右外等等) 表名2 别名2 on 关联条件 where 查询条件 g ...

  5. 创建第一个Scrapy项目

    d:进入D盘 scrapy startproject tutorial建立一个新的Scrapy项目 工程的目录结构: tutorial/ scrapy.cfg # 部署配置文件 tutorial/ # ...

  6. 第一次c++作业(感觉不是很好系列)

    日常先贴github的地址 https://github.com/egoistor/Elevator-scheduling 然后我觉得学习了半天,构造函数似懂非懂(用的是class自动生成的构造函数, ...

  7. Java微笔记(8)

    Java 中的包装类 Java 为每个基本数据类型都提供了一个包装类,这样就可以像操作对象那样来操作基本数据类型 基本类型和包装类之间的对应关系: 包装类主要提供了两大类方法: 将本类型和其他基本类型 ...

  8. springboot+vue+element:echarts开发遇见问题---vue前端(二)

    <template> <u-grid> <u-grid-item caption="服务使用统计排行"> <div class=" ...

  9. Sqlserver学习研究

    关注关键词 :Sqlserver实用工具配置步骤 1)创建实用工具控制点(UCP) 2)连接到现有UCP 3)相UCP注册SQL Server实例 4)创建数据层应用程序 5)设置资源运行状况策略 6 ...

  10. 团队作业4——第一次项目冲刺(Alpha版本)第三次

    一.会议内容 制定任务内容 制作leangoo表格 初步工作 二.各人工作 成员 计划任务 遇见难题 贡献比 塗家瑜(组长) api搭建 无 1 张新磊 数据库搭建完成 无 1 姚燕彬 功能测试 无 ...