Android SharePrefence 源码分析

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() {
                @Override
                public 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() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                      ......
                    }
                };

            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public 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 的生命周期中强行执行提交:

@Override
    public 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 的思路,当一个文件要频繁被读写的时候,就可能出现文件损坏的情况发生(例如数据库),这个时候就需要备份文件的设计,确保文件数据尽量不被丢失。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值