}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, “Attempt to read preferences file " + mFile + " without permission”);
}
Map map = null;
StructStat stat = null;
try {
// 获取文件信息,包括文件修改时间,文件大小等
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
// 读取数据并且将数据解析为jia
str = new BufferedInputStream(
new FileInputStream(mFile), );
map = XmlUtils.readMapXml(str);
} catch (XmlPullParserException | IOException e) {
Log.w(TAG, “getSharedPreferences”, e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/ ignore */
}
synchronized (SharedPreferencesImpl.this) {
// 加载数据成功,设置 mLoaded 为 true
mLoaded = true;
if (map != null) {
// 将解析得到的键值对数据赋值给 mMap
mMap = map;
// 将文件的修改时间戳保存到 mStatTimestamp 中
mStatTimestamp = stat.st_mtime;
// 将文件的大小保存到 mStatSize 中
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
// 通知唤醒所有等待的线程
notifyAll();
}
}
上面的源码中,我们对startLoadFromDisk()
方法进行了分析,有分析我们可以得到以下几点总结:
- 如果有备份文件,直接使用备份文件进行回滚
- 第一次调用
getSharedPreferences
方法的时候,会从磁盘中加载数据,而数据的加载时通过开启一个子线程调用loadFromDisk
方法进行异步读取的 - 将解析得到的键值对数据保存在
mMap
中 - 将文件的修改时间戳以及大小分别保存在
mStatTimestamp
以及mStatSize
中(保存这两个值有什么用呢?我们在分析getSharedPreferences
方法时说过,如果有其他进程修改了文件,并且mode
为MODE_MULTI_PROCESS
,将会判断重新加载文件。如何判断文件是否被其他进程修改过,没错,根据文件修改时间以及文件大小即可知道) - 调用
notifyAll()
方法通知唤醒其他等待线程,数据已经加载完毕
好了,至此,我们就解决了第一个疑问:调用**ContextImpl.getSharedPreferences**
方法获取一个**SharedPreferences**
对象的过程,系统做了什么工作?
下面给出一个时序流程图:
疑问2:getXxx做了什么?
我们以getString
来分析这个问题:
@Nullable
public String getString(String key, @Nullable String defValue) {
// synchronize 关键字用于保证 getString 方法是线程安全的
synchronized (this) {
// 方法 awaitLoadedLocked() 用于确保加载完数据并保存到 mMap 中才进行数据读取
awaitLoadedLocked();
// 根据 key 从 mMap中获取 value
String v = (String)mMap.get(key);
// 如果 value 不为 null,返回 value,如果为 null,返回默认值
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
// 前面我们说过,mLoaded 代表数据是否已经加载完毕
while (!mLoaded) {
try {
// 等待数据加载完成之后才返回继续执行代码
wait();
} catch (InterruptedException unused) {
}
}
}
getString
方法代码很简单,其他的例如getInt
,getFloat
方法也是一样的原理,我们直接对这个疑问进行总结:
getXxx
方法是线程安全的,因为使用了synchronize
关键字getXxx
方法是直接操作内存的,直接从内存中的mMap
中根据传入的key
读取value
getXxx
方法有可能会卡在awaitLoadedLocked
方法,从而导致线程阻塞等待(**什么时候会出现这种阻塞现象呢?前面我们分析过,第一次调用getSharedPreferences
方法时,会创建一个线程去异步加载数据,那么假如在调用完getSharedPreferences
方法之后立即调用getXxx
**方法,此时的mLoaded
很有可能为false
,这就会导致awaiteLoadedLocked
方法阻塞等待,直到loadFromDisk
方法加载完数据并且调用notifyAll
来唤醒所有等待线程)
疑问3:putXxx方法做了什么?
说到写操作方法,首先想到的是通过sharedPreferences.edit()
方法返回的SharedPreferences.Editor
,所有我们对SharedPreferences
的写操作都是基于这个Editor
类的。在 Android 系统中,Editor
是一个接口类,它的具体实现类是EditorImpl
:
public final class EditorImpl implements Editor {
// putXxx/remove/clear等写操作方法都不是直接操作 mMap 的,而是将所有
// 的写操作先记录在 mModified 中,等到 commit/apply 方法被调用,才会将
// 所有写操作同步到 内存中的 mMap 以及磁盘中
private final Map<String, Object> mModified = Maps.newHashMap();
//
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
public Editor putStringSet(String key, @Nullable Set values) {
synchronized (this) {
mModified.put(key, (values == null) ? null : new HashSet(values));
return this;
}
}
public Editor putInt(String key, int value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
public Editor putLong(String key, long value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
public Editor putFloat(String key, float value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
public Editor putBoolean(String key, boolean value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
public Editor remove(String key) {
synchronized (this) {
mModified.put(key, this);
return this;
}
}
…
其他方法
…
}
从EditorImpl
类的源码我们可以得出以下总结:
SharedPreferences
的写操作是线程安全的,因为使用了synchronize
关键字- 对键值对数据的增删记录保存在
mModified
中,而并不是直接对SharedPreferences.mMap
进行操作(mModified
会在commit/apply
方法中起到同步内存SharedPreferences.mMap
以及磁盘数据的作用)
疑问4:commit()/apply()方法如何实现同步/异步写磁盘?
commit()方法分析
先分析commit()
方法,直接上源码:
public boolean commit() {
// 前面我们分析 putXxx 的时候说过,写操作的记录是存放在 mModified 中的
// 在这里,commitToMemory() 方法就负责将 mModified 保存的写记录同步到内存中的 mMap 中
// 并且返回一个 MemoryCommitResult 对象
MemoryCommitResult mcr = commitToMemory();
// enqueueDiskWrite 方法负责将数据落地到磁盘上
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()
方法的主体结构很清晰简单:
-
首先将写操作记录同步到内存的
SharedPreferences.mMap
中(将mModified
同步到mMap
) -
然后调用
enqueueDiskWrite
方法将数据写入到磁盘上 -
同步等待写磁盘操作完成(这就是为什么
commit()
方法会同步阻塞等待的原因) -
通知监听者(可以通过
registerOnSharedPreferenceChangeListener
方法注册监听) -
最后返回执行结果:
true
orfalse
看完了commit()
,我们接着来看一下它调用的commitToMemory()
方法:
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
// We optimistically don’t make a deep copy until
// a memory commit comes in when we’re already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can’t modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
mMap = new HashMap<String, Object>(mMap);
}
// 将 mMap 赋值给 mcr.mapToWriteToDisk,mcr.mapToWriteToDisk 指向的就是最终写入磁盘的数据
mcr.mapToWriteToDisk = mMap;
// mDiskWritesInFlight 代表的是“此时需要将数据写入磁盘,但还未处理或未处理完成的次数”
// 将 mDiskWritesInFlight 自增1(这里是唯一会增加 mDiskWritesInFlight 的地方)
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList();
mcr.listeners =
new HashSet(mListeners.keySet());
}
synchronized (this) {
// 只有调用clear()方法,mClear才为 true
if (mClear) {
if (!mMap.isEmpty()) {
mcr.changesMade = true;
// 当 mClear 为 true,清空 mMap
mMap.clear();
}
mClear = false;
}
// 遍历 mModified
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey(); // 获取 key
Object v = e.getValue(); // 获取 value
// 当 value 的值是 “this” 或者 null,将对应 key 的键值对数据从 mMap 中移除
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else { // 否则,更新或者添加键值对数据
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}
// 将 mModified 同步到 mMap 之后,清空 mModified 历史记录
mModified.clear();
}
}
return mcr;
}
总的来说,commitToMemory()
方法主要做了这几件事:
mDiskWritesInFlight
自增1(mDiskWritesInFlight
代表“此时需要将数据写入磁盘,但还未处理或未处理完成的次数”,提示,整个SharedPreferences
的源码中,唯独在commitToMemory()
方法中“有且仅有”一处代码会对mDiskWritesInFlight
进行增加,其他地方都是减)- 将
mcr.mapToWriteToDisk
指向mMap
,mcr.mapToWriteToDisk
就是最终需要写入磁盘的数据 - 判断
mClear
的值,如果是true
,清空mMap
(调用clear()
方法,会设置mClear
为true
) - 同步
mModified
数据到mMap
中,然后清空mModified
- 最后返回一个
MemoryCommitResult
对象,这个对象的mapToWriteToDisk
参数指向了最终需要写入磁盘的mMap
需要注意的是,在commitToMemory()
方法中,当mClear
为true
,会清空mMap
,但不会清空mModified
,所以依然会遍历mModified
,将其中保存的写记录同步到mMap
中,所以下面这种写法是错误的:
sharedPreferences.edit()
.putString(“key1”, “value1”) // key1 不会被 clear 掉,commit 之后依旧会被写入磁盘中
.clear()
.commit();
分析完commitToMemory()
方法,我们再回到commit()
方法中,对它调用的enqueueDiskWrite
方法进行分析:
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
// 创建一个 Runnable 对象,该对象负责写磁盘操作
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
// 顾名思义了,这就是最终通过文件操作将数据写入磁盘的方法了
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
// 写入磁盘后,将 mDiskWritesInFlight 自减1,代表写磁盘的需求减少一个
mDiskWritesInFlight–;
}
if (postWriteRunnable != null) {
// 执行 postWriteRunnable(提示,在 apply 中,postWriteRunnable 才不为 null)
postWriteRunnable.run();
}
}
};
// 如果传进的参数 postWriteRunnable 为 null,那么 isFromSyncCommit 为 true
// 温馨提示:从上面的 commit() 方法源码中,可以看出调用 commit() 方法传入的 postWriteRunnable 为 null
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
// 如果此时只有一个 commit 请求(注意,是 commit 请求,而不是 apply )未处理,那么 wasEmpty 为 true
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 当只有一个 commit 请求未处理,那么无需开启线程进行处理,直接在本线程执行 writeToDiskRunnable 即可
writeToDiskRunnable.run();
return;
}
}
// 将 writeToDiskRunnable 方法线程池中执行
// 程序执行到这里,有两种可能:
// 1. 调用的是 commit() 方法,并且当前只有一个 commit 请求未处理
// 2. 调用的是 apply() 方法
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
上面的注释已经说得很明白了,在这里就不总结了,接着来分析下writeToFile
这个方法:
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 (!mcr.changesMade) {
// If the file already exists, but no changes were
// made to the underlying map, it’s wasteful to
// re-write the file. Return as if we wrote it
// out.
mcr.setDiskWriteResult(true);
return;
}
if (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn’t rename file " + mFile
- " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false);
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);
if (str == null) {
mcr.setDiskWriteResult(false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Libcore.os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
mcr.setDiskWriteResult(true);
return;
} catch (XmlPullParserException e) {
Log.w(TAG, “writeToFile: Got exception:”, e);
} catch (IOException e) {
Log.w(TAG, “writeToFile: Got exception:”, e);
}
// Clean up an unsuccessfully written file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn’t clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false);
}
-
先把已存在的老的 SP 文件重命名(加“.bak”后缀),然后删除老的 SP 文件,这相当于做了备份(灾备)
-
向
mFile
中一次性写入所有键值对数据,即mcr.mapToWriteToDisk
(这就是commitToMemory
所说的保存了所有键值对数据的字段) 一次性写入到磁盘。 如果写入成功则删除备份(灾备)文件,同时记录了这次同步的时间 -
如果往磁盘写入数据失败,则删除这个半成品的 SP 文件
通过上面的分析,我们对commit()
方法的整个调用链以及它干了什么都有了认知,下面给出一个图方便记忆理解:
apply()方法分析
分析完commit()
方法,再去分析apply()
方法就轻松多了:
public void apply() {
// 将 mModified 保存的写记录同步到内存中的 mMap 中,并且返回一个 MemoryCommitResult 对象
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
结尾
我还总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料分享给大家。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
(InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
结尾
我还总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料分享给大家。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。
[外链图片转存中…(img-iuHv8kE1-1715431919871)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!