SharedPreferences源码分析
分析达成目标
- 了解基本实现
- SharePreferences是否线程安全
- SharePreferences的mode参数是什么
- 了解apply与commit的区别
- 导致ANR的原因
- Android8.0做了什么优化
基本实现
简单使用
先从如何简单使用开始
val sp = context.getSharedPreferences("123", Context.MODE_PRIVATE)
//通过SharedPreferences读值
val myValue = sp.getInt("myKey",-1)
//通过SharedPreferences.Editor写值
sp.edit().putInt("myKey",1).apply()
SharedPreferences对象从哪里来
SharedPreferences只是一个有各种get方法的接口,结构是这样的
//SharedPreferences.java
public interface SharedPreferences {
int getInt(String key, int defValue);
Map<String, ?> getAll();
public interface Editor {
Editor putString(String key, @Nullable String value);
Editor putInt(String key, int value);
}
}
那么它从哪里来,我们得到context具体实现类ContextImpl里去找,以下代码都会省略不必要的部分
//ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
//可以看到返回的SharedPreferences其实就是一个SharedPreferencesImpl实例
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//每个File都对应着一个SharedPreferencesImpl实例
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
return sp;
}
从上面可以看出
- SharedPreferences真正实现是SharedPreferencesImpl
- 对于同一个进程来说,SharedPreferencesImpl和同一个文件是一一对应的
SharedPreferencesImpl
内部存储了一个Map用于把数据缓存到内存
//SharedPreferencesImpl.java
@GuardedBy("mLock")//操作时通过mLock对象锁保证线程安全
Map<String, Object> mMap
对于同一个SharedParences.Editor来说,每个Editor也包含了一个map用来保存本次改变的数据
//SharedPreferencesImpl.java
@GuardedBy("mEditorLock")//操作时通过mEditorLock对象锁保证线程安全
Map<String, Object> mModified
getInt
//SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//如果正在从xml文件中同步map到内存,则会阻塞等待同步完成
awaitLoadedLocked();
//直接从内存mMap中拿数据
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
从上面代码可以看出,SharedPreferences会优先从内存中拿数据
Editor.putInt
public Editor putInt(String key, int value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
putInt只是存入了mModified中,并没有进行其它操作
Editor.apply
//SharedPreferencesImpl.java
public void apply() {
//1. 遍历mModified
//2. 合并修改到mMap中
//3. 当前memory的代数 mCurrentMemoryStateGeneration++
//以此完成内存的实现。返回的MemoryCommitResult用于之后的xml文件写入
final MemoryCommitResult mcr = commitToMemory();
//这里是一个纯粹等待xml写入用的任务
//writtenToDiskLatch只在本次Editor的修改完全写入到文件后释放
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
//把上面的任务加入到QueuedWork的finisher列表中
//ActivityThread在调用Activity的onPause、onStop,或者Service的onStop之前都会调用QueuedWork的waitToFinish
//waitToFinish方法则会轮流遍历运行它们的run方法,即在主线程触发await
QueuedWork.addFinisher(awaitCommit);
//在上一个等待任务外面再封装一层等待任务,用于在写入文件完成后从QueuedWork里移除finish
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
//若成功完成,则从QueuedWork里移除该finisher
QueuedWork.removeFinisher(awaitCommit);
}
};
//把写入磁盘的任务提交去执行,commit就不会带第二个参数,后面会说这里
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
//写入内存就直接触发回调监听
notifyListeners(mcr);
}
Editor.commit
//SharedPreferencesImpl.java
@Override
public boolean commit() {
//与apply相同,直接写入内存
MemoryCommitResult mcr = commitToMemory();
//直接提交disk任务给线程进行处理,第二个参数为空,表示自己是同步的
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
//注意这里是与apply的不同,直接自己触发await,不再放到Runnable里
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
SharedPreferencesImpl.this.enqueueDiskWrite
执行写入磁盘的任务
//SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//通过第二个参数来判断是apply还是commit,即是否是同步提交
final boolean isFromSyncCommit = (postWriteRunnable == null);
//这个runnable就是写入磁盘的任务
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//关键方法:写入磁盘
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
//这个值在写入一次内存后+1,写入一次磁盘后-1,表示当前正在等待写入磁盘的任务个数
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
//与QueuedWork的waitToFinish不同,这里是在子线程等待写入磁盘任务的完成
postWriteRunnable.run();
}
}
};
// 下面的条件我判断只有当前是最后一次commit任务才会到当前线程执行
// 而commit正常情况下是同步进行的,因此只要之前的apply任务未执行完成,也会改为异步执行
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
//此次同步任务为当前所有任务的最后一次
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//直接在当前线程执行写入xml操作
writeToDiskRunnable.run();
return;
}
}
//这里是把写入xml文件的任务放到QueuedWork的子线程去执行。
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
//8.0之前则是直接用单线程池去执行
//QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
QueuedWork
QueuedWork更像是一个等待任务的集合,其内部含有两个列表
//写入磁盘任务会存入这个列表中,在8.0之前没有这个列表,只有一个SingleThreadExecutor线程池用来执行xml写入任务
private static final LinkedList<Runnable> sWork = new LinkedList<>();
//等待任务会存入这个列表中
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
//插入一个磁盘写入的任务,会放到QueuedWork里的一个HandlerThread去执行
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
//如果是apply则100ms后再触发去遍历执行等待任务,commit则不延迟
if (shouldDelay && sCanDelay) {
//这里只需要知道是触发执行sWork里的所有任务,即写入磁盘任务
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
而等待任务列表sFinishers会在waitToFinish方法中使用到,作用是直接去执行所有磁盘任务,执行完成之后再轮流执行所有等待任务
//SharedPreferencesImpl.java
public static void waitToFinish() {
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// 由于我们会手动执行所有的磁盘任务,所以不再需要这些触发执行任务的消息
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
}
// 执行此方法的过程中若插入了其它任务,都不需要再延迟了,直接去触发执行
sCanDelay = false;
}
//遍历执行当前的所有等待硬盘任务的run方法
processPendingWork();
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
//所有任务执行完成之后,道路通畅了,这次waitToFinish执行通过,可以继续延迟100ms
sCanDelay = true;
}
}
以下是Android8.0之前的waitToFinish,只是遍历执行所有等待任务,也不会去主动写入xml,从而导致ANR出现
public static void waitToFinish() {
Runnable toFinish;
//只是去轮流执行所有等待任务
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}
mode权限
我们会通过context获取SharedPreferences对象时传入mode
context.getSharedPreferences("123", Context.MODE_PRIVATE)
该mode会在生成SharedPreferencesImpl实例时传入
//SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
//...
mMode = mode;
}
在xml文件写入完成后调用
//SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
//写入文件
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
str.close();
//给文件加权限
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
}
加权限的过程就终相当于我们在串口使用chmod给权限
//ConetxtImpl.java
static void setFilePermissionsFromMode(String name, int mode,
int extraPermissions) {
//默认给了同一用户与同一群组的读写权限
int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
|FileUtils.S_IRGRP|FileUtils.S_IWGRP
|extraPermissions;
if ((mode&MODE_WORLD_READABLE) != 0) {
//其它用户读权限
perms |= FileUtils.S_IROTH;
}
if ((mode&MODE_WORLD_WRITEABLE) != 0) {
//其它用户写权限
perms |= FileUtils.S_IWOTH;
}
FileUtils.setPermissions(name, perms, -1, -1);
}
//FileUtils.java
public static int setPermissions(String path, int mode, int uid, int gid) {
Os.chmod(path, mode);
return 0;
}
总结
基本实现
SharedPreference有一个内存缓存mMap,以及一个硬盘缓存xml文件。每次通过apply或者commit提交一次editor修改,都会先合入mMap即内存中,之后再缓存到硬盘。注意提交会触发整个文件的修改,因此多个修改最好放在同一个Editor对象中。
线程安全
SharedPreferences主要通过对象锁来保证线程安全,Editor修改时用的是另一个对象锁
,写入disk时也用的是另一个对象锁。
mode是什么
mode类似于给通过chmod给xml文件不同的权限,从而实现其他应用也可以访问的效果,默认MODE_PRIVATE给的是所有者和群组的读写权限,而MODE_WORLD_READABLE与MODE_WORLD_WRITEABLE分别给了其它用户的读写权限
apply与commit
commit与apply的不同主要在于:commit直接在自己的线程等待写入硬盘任务的执行,且commit一次就写一次。而apply不会等待写入硬盘,且8.0之后会根据当前最新的内存代数来过滤掉之前的所有内存修改,只保存最后一次内存修改。
导致ANR的原因
apply提交时会生成一个等待任务放到QueuedWork的一个等待列表里,在Activity的pause、Stop,或者Service的stop执行时,会依次调用这个等待列表的任务,保证每个等待列表所等待的任务都可以执行。若未执行完毕则会导致ANR
Android8.0做了什么优化
8.0的优化方案为
- 若提交了多个apply,在执行时只会执行最后一次提交,减少了文件的写入次数
- QueuedWork优化:执行写入磁盘的任务时,不再直接放到线程池执行,而是先放入一个真实任务的List,在waitToFinish调用时,会主动执行这些真实任务,再执行所有等待任务。而8.0之前只会执行等待任务,对推动任务执行没有任何帮助
参考
SharedPreferences ANR问题分析和解决 & Android 8.0的优化
SharedPreferences源码分析的更多相关文章
- 二维码zxing源码分析(二)decode部分
在上一篇博客中分析了zxing怎么打开摄像头,并且扫描结果,那么扫描之后的数据呢,是不是就要解析了呢,那我们看一下zxing怎么解析这个数据的. 上一篇博客地址ZXING源码 ...
- 关于ContextImp的源码分析
关于ContextImp的源码分析 来源: http://blog.csdn.net/qinjuning/article/details/7310620 Context概述: Android ...
- Android分包MultiDex源码分析
转载请标明出处:http://blog.csdn.net/shensky711/article/details/52845661 本文出自: [HansChen的博客] 概述 Android开发者应该 ...
- Android源码分析(十四)----如何使用SharedPreferencce保存数据
一:SharedPreference如何使用 此文章只是提供一种数据保存的方式, 具体使用场景请根据需求情况自行调整. EditText添加saveData点击事件, 保存数据. diff --git ...
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- nginx源码分析之网络初始化
nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
随机推荐
- Fowsniff靶机
Fowsniff靶机 主机探测+端口扫描. 扫目录没扫到什么,看一下页面源代码. 网站主页告诉我们这个站现在不提供服务了,并且因为收到了安全威胁,攻击者将他们管理员信息发布到了社交媒体上. 大家要科学 ...
- DVWA从注入到GETSHELL
好好过你的生活,不要老是忙着告诉别人你在干嘛. 最近在复习学过的东西,自己就重新搭了个dvwa来学习新思路,写一些简单的脚本来练习写代码的能力. 众所周知SQL注入的危害是相当大的,对于每个老司机来说 ...
- C++雾中风景15:聊聊让人抓狂的Name Mangling
Name Mangling,直接翻译过来为名字改写 .它是深入理解 C++ 编译链接模型的必由之路. 笔者近期进行数据库开发工作时,涉及到MySQL客户端的编译链接的问题,通过重新厘清了之前理解一知半 ...
- 【Vulhub】CVE-2019-3396 Confluence RCE漏洞复现
CVE-2019-3396 Confluence RCE漏洞复现 一.环境搭建 选择的vulhub里的镜像,进入vulhub/Confluence/CVE-2019-3396目录下,执行 docker ...
- Linux知识点笔记
Linux启动脚本 rcS文件,rcS文件位于系统根目录下的"/etc/init.d"下. rcS文件本质是一个bash shell脚本,因此遵循bash脚本的语法规则. [1] ...
- 认证授权:IdentityServer4 - 数据持久化
前言: 前面的文章中IdentityServer4 配置内容都存储到内存中,本篇文章开始把配置信息存储到数据库中:本篇文章继续基于github的代码来实现配置数据持久化到MySQL中 一.基于EFCo ...
- 050 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 12 continue语句
050 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 12 continue语句 本文知识点:continue语句 continue语句 continue ...
- P4231 三步必杀
题目描述 问题摘要: N个柱子排成一排,一开始每个柱子损伤度为0. 接下来勇仪会进行M次攻击,每次攻击可以用4个参数l,r,s,e来描述: 表示这次攻击作用范围为第l个到第r个之间所有的柱子(包含l, ...
- 翻了翻element-ui源码,发现一个很实用的指令clickoutside
前言 指令(directive)在 vue 开发中是一项很实用的功能,指令可以绑定到某一元素或组件,使功能的颗粒度更精细.今天在翻 element-ui 的源码时,发现一个还挺实用的工具指令,跟大伙分 ...
- 手工实现docker的vxlan
前几天了解了一下docker overlay的原理,然后一直想验证一下自己的理解是否正确,今天模仿docker手工搭建了一个overlay网络.先上拓扑图,其实和上次画的基本一样.我下面提到的另一台机 ...