Sp效率分析和理解

2次阅读

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

目录介绍

  • 01.Sp 简单介绍

    • 1.1 Sp 作用分析
    • 1.2 案例分析思考
  • 02.Sp 初始化操作

    • 2.1 如何获取 sp
    • 2.2 SharedPreferencesImpl 构造
  • 03.edit 方法源码
  • 04.put 和 get 方法源码

    • 4.1 put 方法源码
    • 4.2 get 方法源码
  • 05.commit 和 apply

    • 5.1 commit 源码
    • 5.2 apply 源码
  • 06. 总结分析

好消息

  • 博客笔记大汇总【16 年 3 月到至今】,包括 Java 基础及深入知识点,Android 技术博客,Python 学习笔记等等,还包括平时开发中遇到的 bug 汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是 markdown 格式的!同时也开源了生活博客,从 12 年起,积累共计 N 篇[近 100 万字,陆续搬到网上],转载请注明出处,谢谢!
  • 链接地址:https://github.com/yangchong2…
  • 如果觉得好,可以 star 一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

01.Sp 简单介绍说明

1.1 Sp 作用分析

  • sp 作用说明

    • SharedPreferences 是 Android 中比较常用的存储方法,它可以用来存储一些比较小的键值对集合,并最终会在手机的 /data/data/package_name/shared_prefs/ 目录下生成一个 xml 文件存储数据。
  • 分析 sp 包含那些内容

    • 获取 SharedPreferences 对象过程中,系统做了什么?
    • getXxx 方法做了什么?
    • putXxx 方法做了什么?
    • commit/apply 方法如何实现同步 / 异步写磁盘?
  • 分析 sp 包含那些源码

    • SharedPreferences 接口
    • SharedPreferencesImpl 实现类
    • QueuedWork 类

1.2 案例分析思考

1.2.1 edit 用法分析
  • 代码如下所示

    long startA = System.currentTimeMillis();
    for (int i=0 ; i<200 ; i++){SharedPreferences preferences = this.getSharedPreferences("testA", 0);
        SharedPreferences.Editor edit = preferences.edit();
        edit.putString("yc"+i,"yangchong"+i);
        edit.commit();}
    long endA = System.currentTimeMillis();
    long a = endA - startA;
    Log.i("测试 A","----"+a);
    
    
    long startB = System.currentTimeMillis();
    SharedPreferences preferencesB = this.getSharedPreferences("testB", 0);
    SharedPreferences.Editor editB = preferencesB.edit();
    for (int i=0 ; i<200 ; i++){editB.putString("yc"+i,"yangchong"+i);
    }
    editB.commit();
    long endB = System.currentTimeMillis();
    long b = endB - startB;
    Log.i("测试 B","----"+b);
    
    
    long startC = System.currentTimeMillis();
    SharedPreferences.Editor editC = null;
    for (int i=0 ; i<200 ; i++){SharedPreferences preferencesC = this.getSharedPreferences("testC", 0);
        if (editC==null){editC = preferencesC.edit();
        }
        editC.putString("yc"+i,"yangchong"+i);
    }
    editC.commit();
    long endC = System.currentTimeMillis();
    long c = endC - startC;
    Log.i("测试 C","----"+c);
  • 然后开始执行操作

    • A 操作和 B 操作,在代码逻辑上应该是一样的,都是想 SP 中写入 200 次不同字段的数据,区别只是在于,A 操作每次都去获取新的 Editor,而 B 操作是只使用一个 Eidtor 去存储。两个操作都分别执行两次。
    • A 操作和 C 操作,在代码逻辑上应该是一样的,都是想 SP 中写入 200 次不同字段的数据,区别只是在于,A 操作每次都去获取新的 Editor,而 C 操作是只使用一个 Editor 去存储,并且只 commit 一次。两个操作都分别执行两次。
    • B 和 C 的操作几乎都是一样的,唯一不同的是 B 操作只是获取一次 preferencesB 对象,而 C 操作则是获取 200 次 preferencesC 操作。
  • 然后看一下执行结果

    2019-08-30 15:08:16.982 3659-3659/com.cheoo.app I/ 测试 A: ----105
    2019-08-30 15:08:17.035 3659-3659/com.cheoo.app I/ 测试 B: ----52
    2019-08-30 15:08:17.069 3659-3659/com.cheoo.app I/ 测试 C: ----34
    2019-08-30 15:08:20.561 3659-3659/com.cheoo.app I/ 测试 A: ----25
    2019-08-30 15:08:20.562 3659-3659/com.cheoo.app I/ 测试 B: ----1
    2019-08-30 15:08:20.564 3659-3659/com.cheoo.app I/ 测试 C: ----2
  • 结果分析

    • 通过 A 和 B 操作进行比较可知:使用 commit()的方式,如果每次都使用 sp.edit()方法获取一个新的 Editor 的话,新建和修改的执行效率差了非常的大。也就是说,存储一个从来没有用过的 Key,和修改一个已经存在的 Key,在效率上是有差别的。
    • 通过 B 和 C 操作进行比较可知:getSharedPreferences 操作一次和多次其实是没有多大的区别,因为在有缓存,如果存在则从缓存中取。
  • 然后看看里面存储值

    • 其存储的值并不是按照顺序的。
    <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <map>
        <string name="yc110">yangchong110</string>
        <string name="yc111">yangchong111</string>
        <string name="yc118">yangchong118</string>
        <string name="yc119">yangchong119</string>
        <string name="yc116">yangchong116</string>
        <string name="yc117">yangchong117</string>
        <string name="yc114">yangchong114</string>
        <string name="yc115">yangchong115</string>
        <string name="yc112">yangchong112</string>
        <string name="yc113">yangchong113</string>
        <string name="yc121">yangchong121</string>
        <string name="yc122">yangchong122</string>
        <string name="yc120">yangchong120</string>
        <string name="yc129">yangchong129</string>
        <string name="yc127">yangchong127</string>
        <string name="yc128">yangchong128</string>
        <string name="yc125">yangchong125</string>
        <string name="yc126">yangchong126</string>
        <string name="yc123">yangchong123</string>
        <string name="yc124">yangchong124</string>
        <string name="yc1">yangchong1</string>
        <string name="yc109">yangchong109</string>
        <string name="yc0">yangchong0</string>
        <string name="yc3">yangchong3</string>
    </map>
1.2.2 commit 和 apply
  • 代码如下所示

    long startA = System.currentTimeMillis();
    for (int i=0 ; i<200 ; i++){SharedPreferences preferences = activity.getSharedPreferences("testA", 0);
        SharedPreferences.Editor edit = preferences.edit();
        edit.putString("yc"+i,"yangchong"+i);
        edit.apply();}
    long endA = System.currentTimeMillis();
    long a = endA - startA;
    Log.i("测试 A","----"+a);
    
    
    long startB = System.currentTimeMillis();
    SharedPreferences preferencesB = activity.getSharedPreferences("testB", 0);
    SharedPreferences.Editor editB = preferencesB.edit();
    for (int i=0 ; i<200 ; i++){editB.putString("yc"+i,"yangchong"+i);
    }
    editB.apply();
    long endB = System.currentTimeMillis();
    long b = endB - startB;
    Log.i("测试 B","----"+b);
    
    
    long startC = System.currentTimeMillis();
    SharedPreferences.Editor editC = null;
    for (int i=0 ; i<200 ; i++){SharedPreferences preferencesC = activity.getSharedPreferences("testC", 0);
        if (editC==null){editC = preferencesC.edit();
        }
        editC.putString("yc"+i,"yangchong"+i);
    }
    editC.apply();
    long endC = System.currentTimeMillis();
    long c = endC - startC;
    Log.i("测试 C","----"+c);
  • 然后看一下执行结果

    2019-08-30 15:17:07.341 5522-5522/com.cheoo.app I/ 测试 A: ----54
    2019-08-30 15:17:07.346 5522-5522/com.cheoo.app I/ 测试 B: ----5
    2019-08-30 15:17:07.352 5522-5522/com.cheoo.app I/ 测试 C: ----6
    2019-08-30 15:17:10.541 5522-5522/com.cheoo.app I/ 测试 A: ----32
    2019-08-30 15:17:10.542 5522-5522/com.cheoo.app I/ 测试 B: ----1
    2019-08-30 15:17:10.543 5522-5522/com.cheoo.app I/ 测试 C: ----1
  • 得出结论

    • 从执行结果可以发现,使用 apply 因为是异步操作,基本上是不耗费时间的,效率上都是 OK 的。从这个结论上来看,apply 影响效率的地方,在 sp.edit()方法。
  • 可以看出多次执行 edit 方法还是很影响效率的。

    • 在 edit()中是有 synchronized 这个同步锁来保证线程安全的,纵观 EditorImpl.java 的实现,可以看到大部分操作都是有同步锁的,但是只锁了 (this),也就是只对当前对象有效,而 edit() 方法是每次都会去重新 new 一个 EditorImpl()这个 Eidtor 接口的实现类。所以效率就应该是被这里影响到了。
    @Override
    public Editor edit() {// TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {awaitLoadedLocked();
        }
    
        return new EditorImpl();}
1.2.3 给出的建议
  • edit()是有效率影响的,所以不要在循环中去调用吃方法,最好将 edit()方法获取的 Editor 对象方在循环之外,在循环中共用同一个 Editor()对象进行操作。
  • commit()的时候,「new-key」和「update-key」的效率是有差别的,但是有返回结果。
  • apply()是异步操作,对效率的影响,基本上是 ms 级的,可以忽略不记。

02.Sp 初始化操作

2.1 如何获取 sp

  • 首先看 ContextWrapper 源码

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {return mBase.getSharedPreferences(name, mode);
    }
  • 然后看一下 ContextImpl 类

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // At least one application in the world actually passes in a null
        // name.  This happened to work because when we generated the file name
        // we would stringify it to "null.xml".  Nice.
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {if (name == null) {name = "null";}
        }
    
        File file;
        synchronized (ContextImpl.class) {if (mSharedPrefsPaths == null) {mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                // 创建一个对应路径 /data/data/packageName/name 的 File 对象
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
    
        // 这里调用了 getSharedPreferences(File file, int mode) 方法
        return getSharedPreferences(file, mode);
    }
  • 然后接着看一下 getSharedPreferences(file, mode)方法源码

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
    
        // 这里使用了 synchronized 关键字,确保了 SharedPreferences 对象的构造是线程安全的
        synchronized (ContextImpl.class) {
    
            // 获取 SharedPreferences 对象的缓存,并复制给 cache
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
    
            // 以参数 file 作为 key,获取缓存对象
            sp = cache.get(file);
    
            if (sp == null) {  // 如果缓存中不存在 SharedPreferences 对象
                checkMode(mode);
                if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {if (isCredentialProtectedStorage()
                            && !getSystemService(UserManager.class)
                            .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {throw new IllegalStateException("SharedPreferences in credential encrypted"
                }
            }

            // 构造一个 SharedPreferencesImpl 对象
            sp = new SharedPreferencesImpl(file, mode);
            // 放入缓存 cache 中,方便下次直接从缓存中获取
            cache.put(file, sp);
            // 返回新构造的 SharedPreferencesImpl 对象
            return sp;
        }
    }

    // 这里涉及到多进程的逻辑
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {// If somebody else (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.

        // 如果由其他进程修改了这个 SharedPreferences 文件,我们将会重新加载它
        sp.startReloadIfChangedUnexpectedly();}

    // 程序走到这里,说明命中了缓存,SharedPreferences 已经创建,直接返回
    return sp;
}
```
  • 这段源码的流程还是清晰易懂的,注释已经说得很明白,这里我们总结一下这个方法的要点:

    • 缓存未命中, 才构造 SharedPreferences 对象,也就是说,多次调用 getSharedPreferences 方法并不会对性能造成多大影响,因为又缓存机制。
    • SharedPreferences 对象的创建过程是线程安全的,因为使用了 synchronize 关键字。
    • 如果命中了缓存,并且参数 mode 使用了 Context.MODE_MULTI_PROCESS,那么将会调用 sp.startReloadIfChangedUnexpectedly()方法,在 startReloadIfChangedUnexpectedly 方法中,会判断是否由其他进程修改过这个文件,如果有,会重新从磁盘中读取文件加载数据。

2.2 SharedPreferencesImpl 构造

  • 看 SharedPreferencesImpl 的构造方法,源码如下所示

    • 将传进来的参数 file 以及 mode 分别保存在 mFile 以及 mMode 中
    • 创建一个.bak 备份文件,当用户写入失败的时候会根据这个备份文件进行恢复工作
    • 将存放键值对的 mMap 初始化为 null
    • 调用 startLoadFromDisk()方法加载数据
    // SharedPreferencesImpl.java
    // 构造方法
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        // 创建灾备文件,命名为 prefsFile.getPath() + ".bak"
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        // mLoaded 代表是否已经加载完数据
        mLoaded = false;
        // 解析 xml 文件得到的键值对就存放在 mMap 中
        mMap = null;
        // 顾名思义,这个方法用于加载 mFile 这个磁盘上的 xml 文件
        startLoadFromDisk();}
    
    // 创建灾备文件,用于当用户写入失败的时候恢复数据
    private static File makeBackupFile(File prefsFile) {return new File(prefsFile.getPath() + ".bak");
    }
  • 然后看一下调用 startLoadFromDisk()方法加载数据

    // SharedPreferencesImpl.java
    private void startLoadFromDisk() {synchronized (this) {mLoaded = false;}
    
        // 注意:这里我们可以看出,SharedPreferences 是通过开启一个线程来异步加载数据的
        new Thread("SharedPreferencesImpl-load") {public void run() {
                // 这个方法才是真正负责从磁盘上读取 xml 文件数据
                loadFromDisk();}
        }.start();}
    
    private void loadFromDisk() {synchronized (SharedPreferencesImpl.this) {
            // 如果正在加载数据,直接返回
            if (mLoaded) {return;}
    
            // 如果备份文件存在,删除原文件,把备份文件重命名为原文件的名字
            // 我们称这种行为叫做回滚
            if (mBackupFile.exists()) {mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
    
        // 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()方法通知唤醒其他等待线程,数据已经加载完毕

03.edit 方法源码

  • 源码方法如下所示

    @Override
    public Editor edit() {// TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {awaitLoadedLocked();
        }
    
        return new EditorImpl();}

04.put 和 get 方法源码

4.1 put 方法源码

  • 就以 putString 为例分析源码。通过 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;
            }
        }
    
        ......
        其他方法
        ......
    }
  • 从 EditorImpl 类的源码我们可以得出以下总结:

    • SharedPreferences 的写操作是线程安全的,因为使用了 synchronize 关键字
    • 对键值对数据的增删记录保存在 mModified 中,而并不是直接对 SharedPreferences.mMap 进行操作(mModified 会在 commit/apply 方法中起到同步内存 SharedPreferences.mMap 以及磁盘数据的作用)

4.2 get 方法源码

  • 就以 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 来唤醒所有等待线程)

05.commit 和 apply

5.1 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
  • 接着来看一下它调用的 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<String>();
                mcr.listeners =
                        new HashSet<OnSharedPreferenceChangeListener>(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
  • 对调用的 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);
    }
    
    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
                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);
}
```
- writeToFile 这个方法大致分为三个过程:
    - 先把已存在的老的 SP 文件重命名(加“.bak”后缀),然后删除老的 SP 文件,这相当于做了备份(灾备)- 向 mFile 中一次性写入所有键值对数据,即 mcr.mapToWriteToDisk(这就是 commitToMemory 所说的保存了所有键值对数据的字段) 一次性写入到磁盘。- 如果写入成功则删除备份(灾备)文件,同时记录了这次同步的时间如果往磁盘写入数据失败,则删除这个半成品的 SP 文件


5.2 apply 源码

  • 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 将方法将所有数据异步写入到磁盘中

06. 总结分析

  • 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。一旦指定, 直接抛异常

其他介绍

01. 关于博客汇总链接

  • 1. 技术博客汇总
  • 2. 开源项目汇总
  • 3. 生活博客汇总
  • 4. 喜马拉雅音频汇总
  • 5. 其他汇总

02. 关于我的博客

  • github:https://github.com/yangchong211
  • 知乎:https://www.zhihu.com/people/…
  • 简书:http://www.jianshu.com/u/b7b2…
  • csdn:http://my.csdn.net/m0_37700275
  • 喜马拉雅听书:http://www.ximalaya.com/zhubo…
  • 开源中国:https://my.oschina.net/zbj161…
  • 泡在网上的日子:http://www.jcodecraeer.com/me…
  • 邮箱:yangchong211@163.com
  • 阿里云博客:https://yq.aliyun.com/users/a… 239.headeruserinfo.3.dT4bcV
  • segmentfault 头条:https://segmentfault.com/u/xi…
  • 掘金:https://juejin.im/user/593943…
正文完
 0