Sp效率分析和理解

目录介绍

  • 01.Sp简单介绍php

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

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

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

    • 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做用说明github

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

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

    • 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操做。
  • 而后看一下执行结果markdown

    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.关于博客汇总连接

02.关于个人博客

相关文章
相关标签/搜索