Android SharePrefence 源码分析

news/2024/11/16 9:40:01/

1. 背景

SharedPreferences 是 Android 中非常重要的轻量级 KV 存储组件,但是 SharedPreferences 也有其性能方面的问题,本文将主要从源码的角度分析 SharedPreferences 的工作原理和存在的问题。

2. 存在的问题

由于业务不断的发展,SharedPreferences 组件开始慢慢出现一些性能或者功能的缺陷,主要表现如下:

性能瓶颈

Google 将 SharedPreferences 作为轻量级 KV 存储组件推出,可是随着业务不断的发展,存储的 KV 越来越多,导致出现了性能上的问题,其中 commit 和 apply 方法均存在性能问题,文件的格式为XML,读写均较耗时。

存储安全问题

SharedPreferences 将 KV 存储在 XML 结构的文件中,没有加解密存储的功能,存在安全问题。

多进程问题

SharedPreferences 不支持跨进程同步问题,KV 组件跨进程是比较强的需求。

3. SharedPreferences 源码分析

下面结合 SharedPreferences 的源码分析其工作过程,并且指出其性能问题的根本原因,本文重点分析其性能相关的问题,分析源码的时候也主要关注这块。

3.1 SharedPreferences 加载

SharedPreferences 具体的实现在 SharedPreferencesImpl 中,初始化代码如下:

@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {mFile = file;mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;mThrowable = null;startLoadFromDisk();
}

其中 mFile 是 SharedPreferences 存储的文件,mBackupFile 是存储文件的备份,主要在更新文件时起作用,防止系统问题 mFile 写不完整导致的数据丢失问题,重点看 startLoadFromDisk() 加载文件的函数。

private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();}

通过 mLock 锁管理文件加载状态 mLoaded,这里开启线程 loadFromDisk() , 主要是这里并不一定要立马使用 SharedPreferences,尽可能减少对调用线程的阻塞,继续看 loadFromDisk() 方法

private void loadFromDisk() {(1)synchronized (mLock) {if (mLoaded) {return;}if (mBackupFile.exists()) {mFile.delete();mBackupFile.renameTo(mFile);}}......try {(2)stat = Os.stat(mFile.getPath());if (mFile.canRead()) {BufferedInputStream str = null;try {str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);map = (Map<String, Object>) XmlUtils.readMapXml(str);} catch (Exception e) {Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);} finally {IoUtils.closeQuietly(str);}}} catch (Throwable t) {thrown = t;}(3)synchronized (mLock) {mLoaded = true;mThrowable = thrown;try {.......} catch (Throwable t) {mThrowable = t;} finally {mLock.notifyAll();}}}

loadFromDisk() 方法整体逻辑分成 3 个部分分析:

  1. 这里有个很关键的 mBackupFile 的判断和使用,SharedPreferences 在更新数据到文件的时候会将原来的文件重命名为 mBackupFile,然后将所有的 KV 全量写入文件中,执行完成后删除 mBackupFile,如果 mBackupFile 存在就说明之前全量写可能失败了,mFile 可能有损坏,所以需要删除 mFile,使用 mBackupFile 。
  2. 主要是从 XML 格式的文件中读取数据并且解析成 map 。
  3. 修改 mLoaded 加载状态,并且唤醒所有等待的线程,这些等待的线程后面说。

3.2 SharedPreferences 读取数据

SharedPreferences 读取数据有很多种方法,基本都是封装过的,这里看下其中的 getString 方法。

public String getString(String key, @Nullable String defValue) {synchronized (mLock) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;}
}

这个方法看起来就是一个阻塞等待 SharedPreferences 加载完成后从 mMap 种获取数据的过程,重点看下 awaitLoadedLocked() 方法的实现。

private void awaitLoadedLocked() {......while (!mLoaded) {try {mLock.wait();} catch (InterruptedException unused) {}}if (mThrowable != null) {throw new IllegalStateException(mThrowable);}}

这里就是判断 SharedPreferences 的加载状态,如果没有加载完成就等待,上面加载流程也分析过了,加载完成后会唤醒所有等待的线程。

3.3 SharedPreferences 存储数据

SharedPreferences 使用 Editor 中的 putXXX 系列方法修改数据,最后通过 commit 或者 apply 方法提交修改。这里可以看出SharedPreferences 最初的设计就是希望多次修改后统一提交。先看一下 commit 的源码

public boolean commit() {......(1)MemoryCommitResult mcr = commitToMemory();(2)SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);try {(3)mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;} finally {......}notifyListeners(mcr);return mcr.writeToDiskResult;
}

这里主要的逻辑就有三部分:

  1. 对比确定当前内存中修改的需要提交的内容。
  2. 添加到写数据的队列中。
  3. 阻塞当前线程等待写数据结束。

看下 MemoryCommitResult 的定义:

private static class MemoryCommitResult {......final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);......void setDiskWriteResult(boolean wasWritten, boolean result) {......writtenToDiskLatch.countDown();}}

上面省略了大部分内容,可以看出在写数据结束后才会唤醒阻塞的线程。

下面看下 SharedPreferencesImpl.this.enqueueDiskWrite 的内容:

private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final boolean isFromSyncCommit = (postWriteRunnable == null);final Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {(1)writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run();}}};if (isFromSyncCommit) {boolean wasEmpty = false;synchronized (mLock) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}(2)QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);}

这段代码基本就看上面标注的两个地方:

  1. 写数据到文件中,这里面的内容不再深入了。
  2. QueuedWork.queue 可以简单理解为一个延迟 100 毫秒单独执行的线程。

所以 commit 方法是同步写数据到文件中,会阻塞当前线程。继续看下 apply 方法:

public void apply() {final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}......}};QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);......}

apply 方法中的 mcr.writtenToDiskLatch.await() 看起来没有阻塞当前的线程,SharedPreferencesImpl.this.enqueueDiskWrite 方法前面分析过是直接调用 QueuedWork.queue 放到另外一个线程中执行。

上面看着是不是 apply 方法没啥问题,提交到线程中执行,不会阻塞当前的线程,也就没有主线程的性能问题了吗?Android 为了提高 apply 方法的提交成功率在 Activity 和 Service 的生命周期中强行执行提交:

@Overridepublic void handleStopActivity(ActivityClientRecord r, int configChanges,PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {......if (!r.isPreHoneycomb()) {QueuedWork.waitToFinish();}......}
private void handleStopService(IBinder token) {mServicesData.remove(token);Service s = mServices.remove(token);if (s != null) {try {s.onDestroy();......QueuedWork.waitToFinish();......} catch (Exception e) {......}} else {Slog.i(TAG, "handleStopService: token=" + token + " not found.");}}

这两个方法均会执行 QueuedWork.waitToFinish() 方法,QueuedWork.waitToFinish() 中会将所有提交的任务阻塞式执行完,所以就可能导致 ANR 的出现。

4. SharedPreferences 源码总结

  1. SharedPreferences 读写的时候都是阻塞状态,直到文件加载完成。
  2. commit 和 apply 方法均可以提交修改,commit 是同步的,apply 是异步的。
  3. commit 和 apply 的提交均存在性能问题。

5. KV 组件使用的建议

  1. 如果要使用 SharedPreferences 作为 KV 组件,要注意多次修改统一提交,减少修改文件次数,同一个 SharedPreferences 存储的数据不应该过多。
  2. 可以使用 MMKV 等高性能的 KV 组件。

6. 框架学习总结

Google 也一直在优化 SharedPreferences 的性能,但是 SharedPreferences 实在是跟不上现在 Android 项目的性能要求和功能要求,索性就开发新的框架 DataStore 来替代。SharedPreferences 中个人主要的收获是 BackupFile 的思路,当一个文件要频繁被读写的时候,就可能出现文件损坏的情况发生(例如数据库),这个时候就需要备份文件的设计,确保文件数据尽量不被丢失。


http://www.ppmy.cn/news/788637.html

相关文章

最大流?费用流?结合二分图?例题

最大流 给出起点&#xff0c;终点&#xff0c;与边&#xff0c;边有最大流量限制&#xff0c;问从起点在不超过边的流量限制的情况下最大能从起点流多少流量到终点 反悔思想&#xff1a;如果我们每次找到一条路径就把这条路径上流量最小的边删去直到没有路径连接起点和终点&am…

【算法与数据结构】20、LeetCode有效的括号

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;括号匹配是使用栈解决的经典问题。做这道题首先要分析什么时候括号不匹配。1、右括号多余 ( { [ ] } )…

获取HTML页面的所有图片地址

获取HTML页面的所有图片地址&#xff1a; getimgsrc() {let htmlstr document.getElementsByTagName(html)[0].innerHTML;let reg /<img.?src(|")?([^"])(|")?(?:\s|>)/g;let arr [];while (tem reg.exec(htmlstr)) {arr.push(tem[2]);}return ar

JavaScript中获取input上传的图片返回的地址

1. HTML文件 <li><label for"uploadFile" class"upload-file">点击上传</label><!-- 在CSS中将input隐藏&#xff0c;UI效果用label展示 --><input type"file" name"upload_file" id"uploadFile&quo…

如何查询网址的图片地址(下载瓦片地图用)

1、用谷歌浏览器打开网址 比如 map.baidu.com 2、按下F12&#xff08;调出源码器&#xff09;编码工具 3、看到Network&#xff0c;在网站上移动位置 4 看到一堆东西&#xff0c;有缩略图的可以打开看看&#xff0c;右键新窗口打开 https://maponline0.bdimg.com/starpic/?q…

js图片尺寸大小获取

//图片流作为参数传递 获取图片尺寸大小提示function getImgTip(bdImgBase) {(async () > {let size await getImageSize(bdImgBase);$(#imgTip).html("当前图片宽高尺寸为[" size.width "px * " size.height "px]");})();}//参数为二进…

js 通过图片链接获取file对象

直接看代码 imgUrlToFile(url) {var self this;var imgLink url;var tempImage new Image();//如果图片url是网络url&#xff0c;要加下一句代码tempImage.crossOrigin "*";//跨域不可用tempImage.onload function() {var base64 self.getBase64Image(tempIma…

那要怎么找到图片URL呢

图片 URL 可以通过以下步骤找到&#xff1a; 右键点击图片并选择“查看图片”。在新标签页中右键点击图片并选择“复制图片地址”。将复制的图片地址粘贴到地址栏中。 请注意&#xff0c;有些图片可能是动态生成的&#xff0c;因此不能通过上述方法获取它们的 URL。在这种情况下…