Sp效率分析和理解

2,401 阅读20分钟

目录介绍

  • 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万字,陆续搬到网上],转载请注明出处,谢谢!
  • 链接地址:github.com/yangchong21…
  • 如果觉得好,可以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 "
                                + "storage are not available until after user is unlocked");
                    }
                }
    
                // 构造一个 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
                            + " to backup file " + mBackupFile);
                    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.关于我的博客