关于android:Android-初代-KV-存储框架-SharedPreferences旧时代的余晖

59次阅读

共计 27388 个字符,预计需要花费 69 分钟才能阅读完成。

本文已收录到  AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 发问。

前言

大家好,我是小彭。

SharedPreferences 是 Android 平台上轻量级的 K-V 存储框架,亦是初代 K-V 存储框架,至今被很多利用沿用。

有的小伙伴会说,SharedPreferences 是旧时代的产物,当初曾经有 DataStore 或 MMKV 等新时代的 K-V 框架,没有学习意义。但我认为,尽管 SharedPreference 这个计划曾经过期,然而并不意味着 SharedPreference 中应用的技术过期。做技术要知其然,更要知其所以然,而不是随声附和,如果要你解释为什么 SharedPreferences 会过期,你能说到什么水平?

不晓得你最近有没有读到一本在技术圈十分火爆的一本新书《安卓传奇 · Android 缔造团队回忆录》,其中就讲了很多 Android 架构演进中设计者的思考。如果你平时也有从设计者的角度思考过“为什么”,那么很多内容会感觉想到一块去了,反之就会感觉无感。


小彭的 Android 交换群 02 群曾经建设啦,公众号回复“加群”退出咱们~


—— 图片援用自电商平台

明天,咱们就来剖析 SharedPreference 源码,在过程中仍然能够学习到十分丰盛的设计技巧。在后续的文章中,咱们会持续剖析其余 K-V 存储框架,请关注。

本文源码剖析基于 Android 10(API 31),并关联剖析局部 Android 7.1(API 25)。


思维导图:


1. 实现 K-V 框架应该思考什么问题?

在浏览 SharedPreference 的源码之前,咱们先思考一个 K-V 框架应该思考哪些问题?

  • 问题 1 – 线程平安: 因为程序个别会在多线程环境中执行,因而框架有必要保障多线程并发平安,并且优化并发效率;
  • 问题 2 – 内存缓存: 因为磁盘 IO 操作是耗时操作,因而框架有必要在业务层和磁盘文件之间减少一层内存缓存;
  • 问题 3 – 事务: 因为磁盘 IO 操作是耗时操作,因而框架有必要将反对屡次磁盘 IO 操作聚合为一次磁盘写回事务,缩小拜访磁盘次数;
  • 问题 4 – 事务串行化: 因为程序可能由多个线程发动写回事务,因而框架有必要保障事务之间的事务串行化,防止先执行的事务笼罩后执行的事务;
  • 问题 5 – 异步写回: 因为磁盘 IO 是耗时操作,因而框架有必要反对后盾线程异步写回;
  • 问题 6 – 增量更新: 因为磁盘文件内容可能很大,因而批改 K-V 时有必要反对部分批改,而不是全量笼罩批改;
  • 问题 7 – 变更回调: 因为业务层可能有监听 K-V 变更的需要,因而框架有必要反对变更回调监听,并且防止出现内存透露;
  • 问题 8 – 多过程: 因为程序可能有多过程需要,那么框架如何保障多过程数据同步?
  • 问题 9 – 可用性: 因为程序运行中存在不可控的异样和 Crash,因而框架有必要尽可能保证系统可用性,尽量保证系统在遇到异样后的数据完整性;
  • 问题 10 – 高效性: 性能永远是要思考的问题,解析、读取、写入和序列化的性能如何进步和衡量;
  • 问题 11 – 安全性: 如果程序须要存储敏感数据,如何保障数据完整性和保密性;
  • 问题 12 – 数据迁徙: 如果我的项目中存在旧框架,如何将数据从旧框架迁徙至新框架,并且保障可靠性;
  • 问题 13 – 研发体验: 是否模板代码简短,是否容易出错。

提出这么多问题后:

你感觉学习 SharedPreferences 有没有价值呢?

如果让你本人写一个 K-V 框架,你会如何解决这些问题呢?

新时代的 MMKV 和 DataStore 框架是否良好解决了这些问题?


2. 从 Sample 开始

SharedPreferences 采纳 XML 文件格式长久化键值对数据,文件的存储地位位于利用沙盒的外部存储 /data/data/<packageName>/shared_prefs/ 地位,每个 XML 文件对应于一个 SharedPreferences 对象。

在 Activity、Context 和 PreferenceManager 中都存在获取 SharedPreferences 对象的 API,它们最终都会走到 ContextImpl 中:

ContextImpl.java

class ContextImpl extends Context {

    // 获取 SharedPreferences 对象
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {// 后文详细分析...}
}

示例代码

SharedPreferences sp = getSharedPreferences("prefs", Context.MODE_PRIVATE);

// 创立事务
Editor editor = sp.edit();
editor.putString("name", "XIAO PENG");
// 同步提交事务
boolean result = editor.commit(); 
// 异步提交事务
// editor.apply()

// 读取数据
String blog = sp.getString("name", "PENG");

prefs.xml 文件内容

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>    
    <string name="name">XIAO PENG</string>
</map>

3. SharedPreferences 的内存缓存

因为磁盘 IO 操作是耗时操作,如果每一次拜访 SharedPreferences 都执行一次 IO 操作就显得没有必要,所以 SharedPreferences 会在业务层和磁盘之间减少一层内存缓存。在 ContextImpl 类中,不仅反对获取 SharedPreferencesImpl 对象,还负责反对 SharedPreferencesImpl 对象的内存缓存。

ContextImpl 中的内存缓存逻辑是绝对简略的:

  • 步骤 1:通过文件名 name 映射文件对应的 File 对象;
  • 步骤 2:通过 File 对象映射文件对应的 SharedPreferencesImpl 对象。

两个映射表:

  • mSharedPrefsPaths:缓存“文件名 to 文件对象”的映射;
  • sSharedPrefsCache:这是一个二级映射表,第一级是包名到 Map 的映射,第二级是缓存“文件对象 to SP 对象”的映射。每个 XML 文件在内存中只会关联一个全局惟一的 SharedPreferencesImpl 对象

持续剖析发现: 尽管 ContextImpl 实现了 SharedPreferencesImpl 对象的缓存复用,但没有实现缓存淘汰,也没有提供被动移除缓存的 API。因而,在 APP 运行过程中,随着拜访的业务范围越来越多,这部分 SharedPreferences 内存缓存的空间也会逐步收缩。这是一个须要留神的问题。

在 getSharedPreferences() 中还有 MODE_MULTI_PROCESS 标记位的解决:

如果是首次获取 SharedPreferencesImpl 对象会间接读取磁盘文件,如果是二次获取 SharedPreferences 对象会复用内存缓存。但如果应用了 MODE_MULTI_PROCESS 多过程模式,则在返回前会查看磁盘文件绝对于最初一次内存批改是否变动,如果变动则阐明被其余过程批改,须要从新读取磁盘文件,以实现多过程下的“数据同步”。

然而这种同步是十分弱的,因为每个过程自身对磁盘文件的写回是非实时的,再加上如果业务层缓存了 getSharedPreferences(…) 返回的对象,更感知不到最新的变动。所以严格来说,SharedPreferences 是不反对多过程的,官网也明确示意不要将 SharedPreferences 用于多过程环境。

SharedPreferences 内存缓存示意图

流程图

ContextImpl.java

class ContextImpl extends Context {

    // SharedPreferences 文件根目录
    private File mPreferencesDir;

    // < 文件名 - 文件 >
    @GuardedBy("ContextImpl.class")
    private ArrayMap<String, File> mSharedPrefsPaths;

    // 获取 SharedPreferences 对象
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // 1、文件名转文件对象
        File file;
        synchronized (ContextImpl.class) {
            // 1.1 查问映射表
            if (mSharedPrefsPaths == null) {mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            // 1.2 缓存未命中,创立 File 对象
            if (file == null) {file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        // 2、获取 SharedPreferences 对象
        return getSharedPreferences(file, mode);
    }
        
    // -> 1.2 缓存未命中,创立 File 对象
    @Override
    public File getSharedPreferencesPath(String name) {return makeFilename(getPreferencesDir(), name + ".xml");
    }

    private File getPreferencesDir() {synchronized (mSync) {// 文件目录:data/data/[package_name]/shared_prefs/
            if (mPreferencesDir == null) {mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }
}

文件对象 to SP 对象:

ContextImpl.java

class ContextImpl extends Context {

    // < 包名 - Map>
    // < 文件 - SharedPreferencesImpl>
    @GuardedBy("ContextImpl.class")
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

    // -> 2、获取 SharedPreferences 对象
    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            // 2.1 查问缓存
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            // 2.2 未命中缓存(首次获取)if (sp == null) {
                // 2.2.1 查看 mode 标记
                checkMode(mode);
                // 2.2.2 创立 SharedPreferencesImpl 对象
                sp = new SharedPreferencesImpl(file, mode);
                // 2.2.3 缓存
                cache.put(file, sp);
                return sp;
            }
        }
        // 3、命中缓存(二次获取)if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            // 判断以后磁盘文件绝对于最初一次内存批改是否变动,如果时则从新加载文件
            sp.startReloadIfChangedUnexpectedly();}
        return sp;
    }

    // 依据包名获取 < 文件 - SharedPreferencesImpl> 映射表
    @GuardedBy("ContextImpl.class")
    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {if (sSharedPrefsCache == null) {sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }
    ...
}

4. 读取和解析磁盘文件

在创立 SharedPreferencesImpl 对象时,构造函数会启动一个子线程去读取本地磁盘文件,一次性将文件中所有的 XML 数据转化为 Map 散列表。

须要留神的是: 如果在执行 loadFromDisk() 解析文件数据的过程中,其余线程调用 getValue 查问数据,那么就必须期待 mLock 锁直到解析完结。

如果单个 SharedPreferences 的 .xml 文件很大的话,就有可能导致查问数据的线程被长时间被阻塞,甚至导致主线程查问时产生 ANR。这也辅证了 SharedPreferences 只适宜保留大量数据,文件过大在解析时会有性能问题。

读取示意图

SharedPreferencesImpl.java

// 指标文件
private final File mFile;
// 备份文件(后文详细分析)private final File mBackupFile;
// 模式
private final int mMode;
// 锁
private final Object mLock = new Object();
// 读取文件标记位
@GuardedBy("mLock")
private boolean mLoaded = false;

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    // 读取并解析文件数据
    startLoadFromDisk();}

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

// -> 读取并解析文件数据(子线程)private void loadFromDisk() {synchronized (mLock) {if (mLoaded) {return;}
        // 1、如果存在备份文件,则复原备份数据(后文详细分析)if (mBackupFile.exists()) {mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    Map<String, Object> map = null;
    if (mFile.canRead()) {
        // 2、读取文件
        BufferedInputStream str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
        // 3、将 XML 数据解析为 Map 映射表
        map = (Map<String, Object>) XmlUtils.readMapXml(str);
        IoUtils.closeQuietly(str);
    }

    synchronized (mLock) {
        mLoaded = true;

        if (map != null) {
            // 应用解析的映射表
            mMap = map;
        } else {
            // 创立空的映射表
            mMap = new HashMap<>();}
        // 4、唤醒期待 mLock 锁的线程
        mLock.notifyAll();}
}

static File makeBackupFile(File prefsFile) {return new File(prefsFile.getPath() + ".bak");
}

查问数据可能会阻塞期待:

SharedPreferencesImpl.java

public String getString(String key, @Nullable String defValue) {synchronized (mLock) {
        // 期待 mLoaded 标记位
        awaitLoadedLocked();
        // 查问数据
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

private void awaitLoadedLocked() {
    //“查看 - 期待”模式
    while (!mLoaded) {
        try {mLock.wait();
        } catch (InterruptedException unused) {}}
}

5. SharedPreferences 的事务机制

是的,SharedPreferences 也有事务操作。

尽管 ContextImpl 中应用了内存缓存,然而最终数据还是须要执行磁盘 IO 长久化到磁盘文件中。如果每一次“变更操作”都对应一次磁盘“写回操作”的话,不仅效率低下,而且没有必要。

所以 SharedPreferences 会应用“事务”机制,将屡次变更操作聚合为一个“事务”,一次事务最多只会执行一次磁盘写回操作。尽管 SharedPreferences 源码中并没有间接体现出“Transaction”之类的命名,然而这就是一种“事务”设计,与命名无关。

5.1 MemoryCommitResult 事务对象

SharedPreferences 的事务操作由 Editor 接口实现。

SharedPreferences 对象自身只保留获取数据的 API,而变更数据的 API 全副集成在 Editor 接口中。Editor 中会将所有的 putValue 变更操作记录在 mModified 映射表中,但不会触发任何磁盘写回操作,直到调用 Editor#commitEditor#apply 办法时,才会一次性以事务的形式发动磁盘写回工作。

比拟非凡的是:

  • 在 remove 办法中:会将 this 指针作为非凡的移除标记位,后续将通过这个 Value 来判断是移除键值对还是批改 / 新增键值对;
  • 在 clear 办法中:只是将 mClear 标记地位位。

能够看到: 在 Editor#commit 和 Editor#apply 办法中,首先都会调用 Editor#commitToMemery() 收集须要写回磁盘的数据,并封装为一个 MemoryCommitResult 事务对象,随后就是依据这个事务对象的信息写回磁盘。

SharedPreferencesImpl.java

final class SharedPreferencesImpl implements SharedPreferences {

    // 创立修改器对象
    @Override
    public Editor edit() {
        // 期待磁盘文件加载实现
        synchronized (mLock) {awaitLoadedLocked();
        }
        // 创立修改器对象
        return new EditorImpl();}

    // 修改器
    // 非动态外部类(会持有外部类 SharedPreferencesImpl 的援用)public final class EditorImpl implements Editor {

        // 锁对象
        private final Object mEditorLock = new Object();

        // 批改记录(将以事务形式写回磁盘)@GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();

        // 革除全副数据的标记位
        @GuardedBy("mEditorLock")
        private boolean mClear = false;

        // 批改 String 类型键值对
        @Override
        public Editor putString(String key, @Nullable String value) {synchronized (mEditorLock) {mModified.put(key, value);
                return this;
            }
        }

        // 批改 int 类型键值对
        @Override
        public Editor putInt(String key, int value) {synchronized (mEditorLock) {mModified.put(key, value);
                return this;
            }
        }

        // 移除键值对
        @Override
        public Editor remove(String key) {synchronized (mEditorLock) {
                // 将 this 指针作为非凡的移除标记位
                mModified.put(key, this);
                return this;
            }
        }

        // 清空键值对
        @Override
        public Editor clear() {synchronized (mEditorLock) {
                // 革除全副数据的标记位
                mClear = true;
                return this;
            }
        }

        ...

        @Override
        public void apply() {// commitToMemory():写回磁盘的数据并封装事务对象
            MemoryCommitResult mcr = commitToMemory();
            // 同步写回,下文详细分析
        }

        @Override
        public boolean commit() {// commitToMemory():写回磁盘的数据并封装事务对象
            final MemoryCommitResult mcr = commitToMemory();
            // 异步写回,下文详细分析
        }
    }
}

MemoryCommitResult 事务对象外围的字段只有 2 个:

  • memoryStateGeneration: 以后的内存版本(在 writeToFile() 中会过滤低于最新的内存版本的有效事务);
  • mapToWriteToDisk: 最终全量笼罩写回磁盘的数据。

SharedPreferencesImpl.java

private static class MemoryCommitResult {
    // 内存版本
    final long memoryStateGeneration;
    // 须要全量笼罩写回磁盘的数据
    final Map<String, Object> mapToWriteToDisk;
    // 同步计数器
    final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

    @GuardedBy("mWritingToDiskLock")
    volatile boolean writeToDiskResult = false;
    boolean wasWritten = false;

    // 后文写回完结后调用
    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        // writeToDiskResult 会作为 commit 同步写回的返回值
        writeToDiskResult = result;
        // 唤醒期待锁
        writtenToDiskLatch.countDown();}
}

5.2 创立 MemoryCommitResult 事务对象

上面,咱们先来剖析创立 Editor#commitToMemery() 中 MemoryCommitResult 事务对象的步骤,外围步骤分为 3 步:

  • 步骤 1 – 筹备映射表

首先,查看 SharedPreferencesImpl#mDiskWritesInFlight 变量,如果 mDiskWritesInFlight == 0 则阐明不存在并发写回的事务,那么 mapToWriteToDisk 就只会间接指向 SharedPreferencesImpl 中的 mMap 映射表。如果存在并发写回,则会深拷贝一个新的映射表。

mDiskWritesInFlight 变量是记录进行中的写回事务数量记录,每执行一次 commitToMemory() 创立事务对象时,就会将 mDiskWritesInFlight 变量会自增 1,并在写回事务完结后 mDiskWritesInFlight 变量会自减 1。

  • 步骤 2 – 合并变更记录

其次,遍历 mModified 映射表将所有的变更记录(新增、批改或删除)合并到 mapToWriteToDisk 中(此时,Editor 中的数据曾经同步到内存缓存中)。

这一步中的关键点是:如果产生无效批改,则会将 SharedPreferencesImpl 对象中的 mCurrentMemoryStateGeneration 最新内存版本自增 1,比最新内存版本小的事务会被视为有效事务。

  • 步骤 3 – 创立事务对象

最初,应用 mapToWriteToDisk 和 mCurrentMemoryStateGeneration 创立 MemoryCommitResult 事务对象。

事务示意图

SharedPreferencesImpl.java

final class SharedPreferencesImpl implements SharedPreferences {

    // 进行中事务计数(在提交事务是自增 1,在写回完结时自减 1)@GuardedBy("mLock")
    private int mDiskWritesInFlight = 0;

    // 内存版本
    @GuardedBy("this")
    private long mCurrentMemoryStateGeneration;

    // 磁盘版本
    @GuardedBy("mWritingToDiskLock")
    private long mDiskStateGeneration;

    // 修改器
    public final class EditorImpl implements Editor {

        // 锁对象
        private final Object mEditorLock = new Object();

        // 批改记录(将以事务形式写回磁盘)@GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();

        // 革除全副数据的标记位
        @GuardedBy("mEditorLock")
        private boolean mClear = false;

        // 获取须要写回磁盘的事务
        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            boolean keysCleared = false;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                // 如果同时存在多个写回事务,则应用深拷贝 
                if (mDiskWritesInFlight > 0) {mMap = new HashMap<String, Object>(mMap);
                }
                // mapToWriteToDisk:须要写回的数据
                mapToWriteToDisk = mMap;
                // mDiskWritesInFlight:进行中事务自增 1
                mDiskWritesInFlight++;

                synchronized (mEditorLock) {
                    // changesMade:标记是否产生无效批改
                    boolean changesMade = false;

                    // 革除全副键值对
                    if (mClear) {
                        // 革除 mapToWriteToDisk 映射表(上面的 mModified 有可能从新减少键值对)if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();}
                        keysCleared = true;
                        mClear = false;
                    }

                    // 将 Editor 中的 mModified 批改记录合并到 mapToWriteToDisk
                    // mapToWriteToDisk 指向 SharedPreferencesImpl 中的 mMap,所以内存缓存越会被批改
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();
                        Object v = e.getValue();
                        if (v == this /* 应用 this 指针作为魔数 */|| v == null) {
                            // 移除键值对
                            if (!mapToWriteToDisk.containsKey(k)) {continue;}
                            mapToWriteToDisk.remove(k);
                        } else {
                            // 新增或更新键值对
                            if (mapToWriteToDisk.containsKey(k)) {Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {continue;}
                            }
                            mapToWriteToDisk.put(k, v);
                        }
                        // 标记产生无效批改
                        changesMade = true;
                        // 记录变更的键值对
                        if (hasListeners) {keysModified.add(k);
                        }
                    }
                    // 重置批改记录
                    mModified.clear();
                    // 如果产生无效批改,内存版本自增 1
                    if (changesMade) {mCurrentMemoryStateGeneration++;}
                    // 记录以后的内存版本
                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified, listeners, mapToWriteToDisk);
        }
    }
}

步骤 2 – 合并变更记录中,存在一种“反直觉”的 clear() 操作:

如果在 Editor 中存在 clear() 操作,并且 clear 前后都有 putValue 操作,就会呈现反常的成果:如以下示例程序,依照直观的预期成果,最终写回磁盘的键值对应该只有 <age>,但事实上最终 <name> 和 <age> 两个键值对都会被写回磁盘。

呈现这个“景象”的起因是:SharedPreferences 事务中没有放弃 clear 变更记录和 putValue 变更记录的程序,所以 clear 操作之前的 putValue 操作仍然会失效。

示例程序

getSharedPreferences("user", Context.MODE_PRIVATE).let {it.edit().putString("name", "XIAOP PENG")
        .clear()
        .putString("age", "18")
        .apply()}

小结一下 3 个映射表的区别:

  • 1、mMap 是 SharedPreferencesImpl 对象中记录的键值对数据,代表 SharedPreferences 的内存缓存;
  • 2、mModified 是 Editor 修改器中记录的键值对变更记录;
  • 3、mapToWriteToDisk 是 mMap 与 mModified 合并后,须要全量笼罩写回磁盘的数据。

6. 两种写回策略

在取得事务对象后,咱们持续剖析 Editor 接口中的 commit 同步写回策略和 apply 异步写回策略。

6.1 commit 同步写回策略

Editor#commit 同步写回绝对简略,外围步骤分为 4 步:

  • 1、调用 commitToMemory() 创立 MemoryCommitResult 事务对象;
  • 2、调用 enqueueDiskWrite(mrc, null) 提交磁盘写回工作(在以后线程执行);
  • 3、调用 CountDownLatch#await() 阻塞期待磁盘写回实现;
  • 4、调用 notifyListeners() 触发回调监听。

commit 同步写回示意图

其实严格来说,commit 同步写回也不相对是在以后线程同步写回,也有可能在后盾 HandlerThread 线程写回。但不论怎么样,对于 commit 同步写回来说,都会调用 CountDownLatch#await() 阻塞期待磁盘写回实现,所以在逻辑上也等价于在以后线程同步写回。

SharedPreferencesImpl.java

public final class EditorImpl implements Editor {

    @Override
    public boolean commit() {
        // 1、获取事务对象(前文已剖析)MemoryCommitResult mcr = commitToMemory();
        // 2、提交磁盘写回工作
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* 写回胜利回调 */);
        // 3、阻塞期待写回实现
        mcr.writtenToDiskLatch.await();
        // 4、触发回调监听器
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }
}

6.2 apply 异步写回策略

Editor#apply 异步写回绝对简单,外围步骤分为 5 步:

  • 1、调用 commitToMemory() 创立 MemoryCommitResult 事务对象;
  • 2、创立 awaitCommit Ruunnable 并提交到 QueuedWork 中。awaitCommit 中会调用 CountDownLatch#await() 阻塞期待磁盘写回实现;
  • 3、创立 postWriteRunnable Runnable,在 run() 中会执行 awaitCommit 工作并将其从 QueuedWork 中移除;
  • 4、调用 enqueueDiskWrite(mcr, postWriteRunnable) 提交磁盘写回工作(在子线程执行);
  • 5、调用 notifyListeners() 触发回调监听。

能够看到不论是调用 commit 还是 apply,最终都会调用 SharedPreferencesImpl#enqueueDiskWrite() 提交磁盘写回工作。

区别在于:

  • 在 commit 中 enqueueDiskWrite() 的第 2 个参数是 null;
  • 在 apply 中 enqueueDiskWrite() 的第 2 个参数是一个 postWriteRunnable 写回完结的回调对象,enqueueDiskWrite() 外部就是依据第 2 个参数来辨别 commit 和 apply 策略。

apply 异步写回示意图

SharedPreferencesImpl.java

@Override
public void apply() {
    // 1、获取事务对象(前文已剖析)final MemoryCommitResult mcr = commitToMemory();
    // 2、提交 aWait 工作
    // 疑难:postWriteRunnable 能够了解,awaitCommit 是什么?final Runnable awaitCommit = new Runnable() {
        @Override
        public void run() {
            // 阻塞线程直到磁盘工作执行结束
            mcr.writtenToDiskLatch.await();}
    };
    QueuedWork.addFinisher(awaitCommit);
    // 3、创立写回胜利回调
    Runnable postWriteRunnable = new Runnable() {
        @Override
        public void run() {
            // 执行 aWait 工作
            awaitCommit.run();
            // 移除 aWait 工作
            QueuedWork.removeFinisher(awaitCommit);
        }
    };

    // 4、提交磁盘写回工作,并绑定写回胜利回调
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable /* 写回胜利回调 */);

    // 5、触发回调监听器
    notifyListeners(mcr);
}

QueuedWork.java

// 提交 aWait 工作(后文详细分析)private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

public static void addFinisher(Runnable finisher) {synchronized (sLock) {sFinishers.add(finisher);
    }
}

public static void removeFinisher(Runnable finisher) {synchronized (sLock) {sFinishers.remove(finisher);
    }
}

这里有一个疑难:

在 apply() 办法中,在执行 enqueueDiskWrite() 前创立了 awaitCommit 工作并退出到 QueudWork 期待队列,直到磁盘写回完结才将 awaitCommit 移除。这个 awaitCommit 工作是做什么的呢?

咱们略微再答复,先持续往下走。

6.3 enqueueDiskWrite() 提交磁盘写回事务

能够看到,不论是 commit 还是 apply,最终都会调用 SharedPreferencesImpl#enqueueDiskWrite() 提交写回磁盘工作。尽管 enqueueDiskWrite() 还没到真正调用磁盘写回操作的中央,但的确创立了与磁盘 IO 相干的 Runnable 工作,外围步骤分为 4 步:

  • 步骤 1:依据是否有 postWriteRunnable 回调辨别是 commit 和 apply;
  • 步骤 2:创立磁盘写回工作(真正执行磁盘 IO 的中央):

    • 2.1 调用 writeToFile() 执行写回磁盘 IO 操作;
    • 2.2 在写回完结后对前文提到的 mDiskWritesInFlight 计数自减 1;
    • 2.3 执行 postWriteRunnable 写回胜利回调;
  • 步骤 3:如果是异步写回,则提交到 QueuedWork 工作队列;
  • 步骤 4:如果是同步写回,则查看 mDiskWritesInFlight 变量。如果存在并发写回的事务,则也要提交到 QueuedWork 工作队列,否则就间接在以后线程执行。

其中步骤 2 是真正执行磁盘 IO 的中央,逻辑也很好了解。不好了解的是,咱们发现除了“同步写回而且不存在并发写回事务”这种非凡状况,其余状况都会交给 QueuedWork 再调度一次。

在通过 QueuedWork#queue 提交工作时,会将 writeToDiskRunnable 工作追加到 sWork 工作队列中。如果是首次提交工作,QueuedWork 外部还会创立一个 HandlerThread 线程,通过这个子线程实现异步的写回工作。这阐明 SharedPreference 的异步写回相当于应用了一个单线程的线程池,事实上在 Android 8.0 以前的版本中就是应用一个 singleThreadExecutor 线程池实现的。

提交工作示意图

SharedPreferencesImpl.java

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    // 1、依据是否有 postWriteRunnable 回调辨别是 commit 和 apply
    final boolean isFromSyncCommit = (postWriteRunnable == null);
    // 2、创立磁盘写回工作
    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {synchronized (mWritingToDiskLock) {
                // 2.1 写入磁盘文件
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                // 2.2 mDiskWritesInFlight:进行中事务自减 1
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                // 2.3 触发写回胜利回调
                postWriteRunnable.run();}
        }
    };

    // 3、同步写回且不存在并发写回,则间接在以后线程
    // 这就是前文提到“commit 也不是相对在以后线程同步写回”的源码出处
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            // 如果存在并发写回的事务,则此处 wasEmpty = false
            wasEmpty = mDiskWritesInFlight == 1;
        }
        // wasEmpty 为 true 阐明以后只有一个线程在执行提交操作,那么就间接在此线程上实现工作
        if (wasEmpty) {writeToDiskRunnable.run();
            return;
        }
    }

    // 4、交给 QueuedWork 调度(同步工作不能够提早)QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit /* 是否能够提早 */);
}

@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {// 稍后剖析}

QueuedWork 调度:

QueuedWork.java

@GuardedBy("sLock")
private static LinkedList<Runnable> sWork = new LinkedList<>();

// 提交工作
// shouldDelay:是否提早
public static void queue(Runnable work, boolean shouldDelay) {Handler handler = getHandler();

    synchronized (sLock) {
        // 入队
        sWork.add(work);
        // 发送 Handler 音讯,触发 HandlerThread 执行工作
        if (shouldDelay && sCanDelay) {handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY /* 100ms */);
        } else {handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}

private static Handler getHandler() {synchronized (sLock) {if (sHandler == null) {
            // 创立 HandlerThread 后盾线程
            HandlerThread handlerThread = new HandlerThread("queued-work-looper", Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();

            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

private static class QueuedWorkHandler extends Handler {
    static final int MSG_RUN = 1;

    QueuedWorkHandler(Looper looper) {super(looper);
    }

    public void handleMessage(Message msg) {if (msg.what == MSG_RUN) {
            // 执行工作
            processPendingWork();}
    }
}

private static void processPendingWork() {synchronized (sProcessingWork) {
        LinkedList<Runnable> work;

        synchronized (sLock) {
            // 创立新的工作队列
            // 这一步是必须的,否则会与 enqueueDiskWrite 抵触
            work = sWork;
            sWork = new LinkedList<>();

            // Remove all msg-s as all work will be processed now
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        // 遍历,按程序执行 sWork 工作队列
        if (work.size() > 0) {for (Runnable w : work) {w.run();
            }
        }
    }
}

比拟不了解的是:

同一个文件的屡次写回串行化能够了解,对于多个文件的写回串行化意义是什么,是不是能够用多线程来写回多个不同的文件?或者这也是 SharedPreferences 是轻量级框架的起因之一,你感觉呢?

6.4 被动期待写回工作完结

当初咱们能够答复 6.1 中遗留的问题:

在 apply() 办法中,在执行 enqueueDiskWrite() 前创立了 awaitCommit 工作并退出到 QueudWork 期待队列,直到磁盘写回完结才将 awaitCommit 移除。这个 awaitCommit 工作是做什么的呢?

要了解这个问题须要治理剖析到 ActivityThread 中的主线程音讯循环:

能够看到,在主线程的 Activity#onPause、Activity#onStop、Service#onStop、Service#onStartCommand 等生命周期状态变更时,会调用 QueudeWork.waitToFinish():

ActivityThread.java

@Override
public void handlePauseActivity(...) {performPauseActivity(r, finished, reason, pendingActions);
    // Make sure any pending writes are now committed.
    if (r.isPreHoneycomb()) {QueuedWork.waitToFinish();
    }
    ...
}

private void handleStopService(IBinder token) {
    ...
    QueuedWork.waitToFinish();
    ActivityManager.getService().serviceDoneExecuting(token, SERVICE_DONE_EXECUTING_STOP, 0, 0);
    ...
}

waitToFinish() 会执行所有 sFinishers 期待队列中的 aWaitCommit 工作,被动期待所有磁盘写回工作完结。在写回工作完结之前,主线程会阻塞在期待锁上,这里也有可能产生 ANR。

被动期待示意图

至于为什么 Google 要在 ActivityThread 中局部生命周期中被动期待所有磁盘写回工作完结呢?官网并没有明确示意,联合头条和抖音技术团队的文章,我比拟偏向于这 2 点解释:

  • 解释 1 – 跨进程同步(次要): 为了保障跨过程的数据同步,要求在组件跳转前,确保以后组件的写回工作必须在以后生命周期内实现;
  • 解释 2 – 数据完整性: 为了避免在组件跳转的过程中可能产生的 Crash 造成未写回的数据失落,要求以后组件的写回工作必须在以后生命周期内实现。

当然这两个解释并不全面,因为就算要求被动期待,也不能保障跨过程实时同步,也不能保障不产生 Crash。

抖音技术团队观点

QueuedWork.java

@GuardedBy("sLock")
private static Handler sHandler = null;

public static void waitToFinish() {
    boolean hadMessages = false;

    Handler handler = getHandler();

    synchronized (sLock) {if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {// Delayed work will be processed at processPendingWork() below
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);
        }
        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }

    // Android 8.0 优化:帮忙子线程执行磁盘写回
    // 作用无限,因为 QueuedWork 应用了 sProcessingWork 锁保障同一时间最多只有一个线程在执行磁盘写回
    // 所以这里应该是尝试在主线程执行,能够晋升线程优先级
    processPendingWork();

    // 执行 sFinshers 期待队列,期待所有写回工作完结
    try {while (true) {
            Runnable finisher;

            synchronized (sLock) {finisher = sFinishers.poll();
            }

            if (finisher == null) {break;}
            // 执行 mcr.writtenToDiskLatch.await();
            // 阻塞线程直到磁盘工作执行结束
            finisher.run();}
    } finally {sCanDelay = true;}
}

Android 7.1 QueuedWork 源码比照:

public static boolean hasPendingWork() {return !sPendingWorkFinishers.isEmpty();
}

7. writeToFile() 捷足先登

最终走到具体调用磁盘 IO 操作的中央了!

7.1 写回步骤

writeToFile() 的逻辑绝对简单一些了。通过简化后,剩下的外围步骤只有 4 大步骤:

  • 步骤 1:过滤有效写回事务:

    • 1.1 事务的 memoryStateGeneration 内存版本小于 mDiskStateGeneration 磁盘版本,跳过;
    • 1.2 同步写回必须写回;
    • 1.3 异步写回事务的 memoryStateGeneration 内存版本版本小于 mCurrentMemoryStateGeneration 最新内存版本,跳过。
  • 步骤 2:文件备份:

    • 2.1 如果不存在备份文件,则将旧文件重命名为备份文件;
    • 2.2 如果存在备份文件,则删除有效的旧文件(上一次写回出并且后处理没有胜利删除的状况)。
  • 步骤 3:全量笼罩写回磁盘:

    • 3.1 关上文件输入流;
    • 3.2 将 mapToWriteToDisk 映射表全量写出;
    • 3.3 调用 FileUtils.sync() 强制操作系统页缓存写回磁盘;
    • 3.4 写入胜利,则删除被封文件(如果没有走到这一步,在未来读取文件时,会从新复原备份文件);
    • 3.5 将磁盘版本记录为以后内存版本;
    • 3.6 写回完结(胜利)。
  • 步骤 4:后处理: 删除写至半途的有效文件。

7.2 写回优化

持续剖析发现,SharedPreference 的写回操作并不是简略的调用磁盘 IO,在保障 “可用性” 方面也做了一些优化设计:

  • 优化 1 – 过滤有效的写回事务:

如前文所述,commit 和 apply 都可能呈现并发批改同一个文件的状况,此时在间断批改同一个文件的事务序列中,旧的事务是没有意义的。为了过滤这些无意义的事务,在创立 MemoryCommitResult 事务对象时会记录过后的 memoryStateGeneration 内存版本,而在 writeToFile() 中就会依据这个字段过滤有效事务,防止了有效的 I/O 操作。

  • 优化 2 – 备份旧文件:

因为写回文件的过程存在不确定的异样(比方内核解体或者机器断电),为了保障文件的完整性,SharedPreferences 采纳了文件备份机制。在执行写回操作之前,会先将旧文件重命名为 .bak 备份文件,在全量笼罩写入新文件后再删除备份文件。

如果写回文件失败,那么在后处理过程中会删除写至半途的有效文件。此时磁盘中只有一个备份文件,而实在文件须要等到下次触发写回事务时再写回。

如果直到利用退出都没有触发下次写回,或者写回的过程中 Crash,那么在前文提到的创立 SharedPreferencesImpl 对象的构造方法中调用 loadFromDisk() 读取并解析文件数据时,会从备份文件复原数据。

  • 优化 3 – 强制页缓存写回:

在写回文件胜利后,SharedPreference 会调用 FileUtils.sync() 强制操作系统将页缓存写回磁盘。

写回示意图

SharedPreferencesImpl.java

// 内存版本
@GuardedBy("this")
private long mCurrentMemoryStateGeneration;

// 磁盘版本
@GuardedBy("mWritingToDiskLock")
private long mDiskStateGeneration;

// 写回事务
private static class MemoryCommitResult {
    // 内存版本
    final long memoryStateGeneration;
    // 须要全量笼罩写回磁盘的数据
    final Map<String, Object> mapToWriteToDisk;
    // 同步计数器
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

    // 后文写回完结后调用
    // wasWritten:是否有执行写回
    // result:是否胜利
    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        // 唤醒期待锁
        writtenToDiskLatch.countDown();}
}

// 提交写回事务
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    ...
    // 创立磁盘写回工作
    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {synchronized (mWritingToDiskLock) {
                // 2.1 写入磁盘文件
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                // 2.2 mDiskWritesInFlight:进行中事务自减 1
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                // 2.3 触发写回胜利回调
                postWriteRunnable.run();}
        }
    };
    ...
}

// 写回文件
// isFromSyncCommit:是否同步写回
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {boolean fileExists = mFile.exists();
    // 如果旧文件存在
    if (fileExists) { 
        // 1. 过滤有效写回事务
        // 是否须要执行写回
        boolean needsWrite = false;

        // 1.1 磁盘版本小于内存版本,才有可能须要写回
        //(只有旧文件存在才会走到这个分支,然而旧文件不存在的时候也可能存在无意义的写回,// 猜想官网是心愿首次创立文件的写回可能及时尽快执行,毕竟只有一个后盾线程)if (mDiskStateGeneration < mcr.memoryStateGeneration) {if (isFromSyncCommit) {
                // 1.2 同步写回必须写回
                needsWrite = true;
            } else {
                // 1.3 异步写回须要判断事务对象的内存版本,只有最新的内存版本才有必要执行写回
                synchronized (mLock) {if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {needsWrite = true;}
                }
            }
        }

        if (!needsWrite) {
            // 1.4 有效的异步写回,间接完结
            mcr.setDiskWriteResult(false, true);
            return;
        }

        // 2. 文件备份
        boolean backupFileExists = mBackupFile.exists();
        if (!backupFileExists) {
            // 2.1 如果不存在备份文件,则将旧文件重命名为备份文件
            if (!mFile.renameTo(mBackupFile)) {
                // 备份失败
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            // 2.2 如果存在备份文件,则删除有效的旧文件(上一次写回出并且后处理没有胜利删除的状况)mFile.delete();}
    }

    try {
        // 3、全量笼罩写回磁盘
        // 3.1 关上文件输入流
        FileOutputStream str = createFileOutputStream(mFile);
        if (str == null) {
            // 关上输入流失败
            mcr.setDiskWriteResult(false, false);
            return;
        }
        // 3.2 将 mapToWriteToDisk 映射表全量写出
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        // 3.3 FileUtils.sync:强制操作系统将页缓存写回磁盘
        FileUtils.sync(str);
        // 敞开输入流
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

        // 3.4 写入胜利,则删除被封文件(如果没有走到这一步,在未来读取文件时,会从新复原备份文件)mBackupFile.delete();
        // 3.5 将磁盘版本记录为以后内存版本
        mDiskStateGeneration = mcr.memoryStateGeneration;
        // 3.6 写回完结(胜利)mcr.setDiskWriteResult(true, true);

        return;
    } catch (XmlPullParserException e) {Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {Log.w(TAG, "writeToFile: Got exception:", e);
    }

    // 在 try 块中抛出异样,会走到这里
    // 4、后处理:删除写至半途的有效文件
    if (mFile.exists()) {if (!mFile.delete()) {Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    // 写回完结(失败)mcr.setDiskWriteResult(false, false);
}

// -> 读取并解析文件数据
private void loadFromDisk() {synchronized (mLock) {if (mLoaded) {return;}
        // 1、如果存在备份文件,则复原备份数据(后文详细分析)if (mBackupFile.exists()) {mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
    ...
}

至此,SharedPreferences 外围源码剖析完结。


8. SharedPreferences 的其余细节

SharedPreferences 还有其余细节值得学习。

8.1 SharedPreferences 锁总结

SharedPreferences 是线程平安的,但它的线程平安并不是间接应用一个全局的锁对象,而是采纳多种颗粒度的锁对象实现 “锁细化”,而且还贴心地应用了 @GuardedBy 注解标记字段或办法所述的锁级别。

应用 @GuardedBy 注解标记锁级别

@GuardedBy("mLock")
private Map<String, Object> mMap;
对象锁 性能呢 形容
1、SharedPreferenceImpl#mLock SharedPreferenceImpl 对象的全局锁 全局应用
2、EditorImpl#mEditorLock EditorImpl 修改器的写锁 确保多线程拜访 Editor 的竞争平安
3、SharedPreferenceImpl#mWritingToDiskLock SharedPreferenceImpl#writeToFile() 的互斥锁 writeToFile() 中会批改内存状态,须要保障多线程竞争平安
4、QueuedWork.sLock QueuedWork 的互斥锁 确保 sFinishers 和 sWork 的多线程资源竞争平安
5、QueuedWork.sProcessingWork QueuedWork#processPendingWork() 的互斥锁 确保同一时间最多只有一个线程执行磁盘写回工作

8.2 应用 WeakHashMap 存储监听器

SharedPreference 提供了 OnSharedPreferenceChangeListener 回调监听器,能够在主线程监听键值对的变更(蕴含批改、新增和移除)。

SharedPreferencesImpl.java

@GuardedBy("mLock")
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
    new WeakHashMap<OnSharedPreferenceChangeListener, Object>();

SharedPreferences.java

public interface SharedPreferences {

    public interface OnSharedPreferenceChangeListener {void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }
}

比拟意外的是: SharedPreference 应用了一个 WeakHashMap 弱键散列表存储监听器,并且将监听器对象作为 Key 对象。这是为什么呢?

这是一种避免内存透露的思考,因为 SharedPreferencesImpl 的生命周期是全局的(位于 ContextImpl 的内存缓存),所以有必要应用弱援用避免内存透露。想想也对,Java 规范库没有提供相似 WeakArrayList 或 WeakLinkedList 的容器,所以这里将监听器对象作为 WeakHashMap 的 Key,就很奇妙的复用了 WeakHashMap 主动清理有效数据的能力。

提醒: 对于 WeakHashMap 的详细分析,请浏览小彭说 · 数据结构与算法 专栏文章《WeakHashMap 和 HashMap 的区别是什么,何时应用?》

8.3 如何查看文件被其余过程批改?

在读取和写入文件后记录 mStatTimestamp 工夫戳和 mStatSize 文件大小,在查看时查看这两个字段是否发生变化

SharedPreferencesImpl.java

// 文件工夫戳
@GuardedBy("mLock")
private StructTimespec mStatTimestamp;
// 文件大小
@GuardedBy("mLock")
private long mStatSize;

// 读取文件
private void loadFromDisk() {
    ...
    mStatTimestamp = stat.st_mtim;
    mStatSize = stat.st_size;
    ...
}

// 写入文件
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    ...
    mStatTimestamp = stat.st_mtim;
    mStatSize = stat.st_size;
    ...
}

// 查看文件
private boolean hasFileChangedUnexpectedly() {synchronized (mLock) {if (mDiskWritesInFlight > 0) {
            // If we know we caused it, it's not unexpected.
            if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
            return false;
        }
    }

    // 读取文件 Stat 信息
    final StructStat stat = Os.stat(mFile.getPath());

    synchronized (mLock) {
        // 查看批改工夫和文件大小
        return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
    }
}

至此,SharedPreferences 全副源码剖析完结。


9. 总结

能够看到,尽管 SharedPreferences 是一个轻量级的 K-V 存储框架,但确实是一个残缺的存储计划。从源码剖析中,咱们能够看到 SharedPreferences 在读写性能、可用性方面都有做一些优化,例如:锁细化、事务化、事务过滤、文件备份等,值得细细品味。

在下篇文章里,咱们来盘点 SharedPreferences 中存在的“毛病”,为什么 SharedPreferences 没有乘上新时代的船只。请关注。


参考资料

  • Android SharedPreferences 的了解与应用 —— ghroosk 著
  • 一文读懂 SharedPreferences 的缺点及一点点思考 —— 业志陈 著
  • 反思|官网也无力回天?Android SharedPreferences 的设计与实现 —— 却把青梅嗅 著
  • 分析 SharedPreference apply 引起的 ANR 问题 —— 字节跳动技术团队
  • 今日头条 ANR 优化实际系列 – 辞别 SharedPreference 期待 —— 字节跳动技术团队

正文完
 0