面试高频题:一眼看穿 SharedPreferences,2024年最新自学Android


其他方法

}

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 or false

看完了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指向mMapmcr.mapToWriteToDisk就是最终需要写入磁盘的数据
  • 判断mClear的值,如果是true,清空mMap(调用clear()方法,会设置mCleartrue
  • 同步mModified数据到mMap中,然后清空mModified
  • 最后返回一个MemoryCommitResult对象,这个对象的mapToWriteToDisk参数指向了最终需要写入磁盘的mMap

需要注意的是,在commitToMemory()方法中,当mCleartrue,会清空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() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};

// 将数据落地到磁盘上,注意,传入的 postWriteRunnable 参数不为 null,所以在
// enqueueDiskWrite 方法中会开启子线程异步将数据写入到磁盘中
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

// Okay to notify the listeners before it’s hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}

总结一下apply()方法:

  • commitToMemory()方法将mModified中记录的写操作同步回写到内存 SharedPreferences.mMap 中。此时, 任何的getXxx方法都可以获取到最新数据了

  • 通过enqueueDiskWrite方法调用writeToFile将方法将所有数据异步写入到磁盘中

下面也给出一个apply()时序流程图帮助记忆理解:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结

  • SharedPreferences是线程安全的,它的内部实现使用了大量synchronized关键字
  • SharedPreferences不是进程安全的
  • 第一次调用getSharedPreferences会加载磁盘 xml 文件(这个加载过程是异步的,通过new Thread来执行,所以并不会在构造SharedPreferences的时候阻塞线程,但是会阻塞getXxx/putXxx/remove/clear等调用),但后续调用getSharedPreferences会从内存缓存中获取。 如果第一次调用getSharedPreferences时还没从磁盘加载完毕就马上调用 getXxx/putXxx, 那么getXxx/putXxx操作会阻塞,直到从磁盘加载数据完成后才返回
  • 所有的getXxx都是从内存中取的数据,数据来源于SharedPreferences.mMap
  • apply同步回写(commitToMemory())内存SharedPreferences.mMap,然后把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。apply不需要等待写入磁盘完成,而是马上返回
  • commit同步回写(commitToMemory())内存SharedPreferences.mMap,然后如果mDiskWritesInFlight(此时需要将数据写入磁盘,但还未处理或未处理完成的次数)的值等于1,那么直接在调用commit的线程执行回写磁盘的操作,否则把异步回写磁盘的任务放到一个单线程的线程池队列中等待调度。commit会阻塞调用线程,知道写入磁盘完成才返回
  • MODE_MULTI_PROCESS是在每次getSharedPreferences时检查磁盘上配置文件上次修改时间和文件大小,一旦所有修改则会重新从磁盘加载文件,所以并不能保证多进程数据的实时同步
  • 从 Android N 开始,,不支持MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE。一旦指定, 直接抛异常
    使用注意事项

  • 不要使用**SharedPreferences****作为多进程通信手段。**由于没有使用跨进程的锁,就算使用MODE_MULTI_PROCESSSharedPreferences在跨进程频繁读写有可能导致数据全部丢失。根据线上统计,SP 大约会有万分之一的损坏率
  • 每个 SP 文件不能过大。SharedPreference的文件存储性能与文件大小相关,我们不要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来
  • **还是每个 SP 文件不能过大。**在第一个getSharedPreferences时,会先加载 SP 文件进内存,过大的 SP 文件会导致阻塞,甚至会导致 ANR
  • **依旧是每个 SP 文件不能过大。**每次apply或者commit,都会把全部的数据一次性写入磁盘, 所以 SP 文件不应该过大, 影响整体性能

喜欢的朋友,点个赞呗鼓励鼓励呗~

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后

感觉现在好多人都在说什么安卓快凉了,工作越来越难找了。又是说什么程序员中年危机啥的,为啥我这年近30的老农根本没有这种感觉,反倒觉得那些贩卖焦虑的都是瞎j8扯谈。当然,职业危机意识确实是要有的,但根本没到那种草木皆兵的地步好吗?

Android凉了都是弱者的借口和说辞。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

所以,最后这里放上我耗时两个月,将自己8年Android开发的知识笔记整理成的Android开发者必知必会系统学习资料笔记,上述知识点在笔记中都有详细的解读,里面还包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

[外链图片转存中…(img-6depkIBT-1712779996685)]

以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-xRHC57Kl-1712779996686)]

  • 16
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值