每日一问:谈谈 SharedPreferences 的 apply() 和 commit()

SharedPreferences灵魂拷问之原理

先来一波灵魂追问:

  • 听说提交要用apply(),为什么?
  • 和commit()什么区别?
  • 跨进程怎么操作?
  • 会堵塞主线程吗?
  • 很着急有替代方案吗?

年末福利: 知道你很忙,参考答案可直接看文末... )

1、加载/初始化

 

每日一问:谈谈 SharedPreferences 的 apply() 和 commit()_第1张图片

 

 

一切从getSharedPreference(String name,int Mode)这个方法说起;通过这个方法获取到一个SharedPreference实例。SharedPreferences是一个接口(interface),他的具体实现类为SharedPreferencesImpl。 SharedPreference的加载的主要过程:

  • 找到对应name的文件。
  • 加载对应文件到内存中SharedPreference。
  • 一个xml文件对应一个ShredPreferences单例
private static ArrayMap> sSharedPrefsCache;
private ArrayMap mSharedPrefsPaths;
复制代码

sSharedPrefsCache存储的是File和SharedPreferencesImpl键值对,当对应File的SharedPreferencesImpl加载之后就会一支存储于sSharedPrefsCache中。类似的mSharedPrefsPaths存储的是name和File的对应关系。使用的ArrayMap,关于ArrayMap这种Android特有的数据结构,详细了解可以看这juejin.im/post/684490…

当通过name最终找到对应的File之后,就会实例化一个SharedPreferencesImpl对象。在SharedPreferences构造方法中开启一个子线程加载磁盘中的xml文件。

大家都应该很明确的一点是,SP持久化的本质是在本地磁盘记录了一个xml文件,这个文件所在的文件夹shared_prefs

 

image

 

 

    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
复制代码

怎么保证使用sp.get(String name)的时候SP的初始化或者说从磁盘中加载到内存中这一过程已经完成了呢?

    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            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();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }
复制代码

使用awaitLoadedLocked()方法检测,是否已经加载完成,如果没有加载完成,就等待堵塞。等加载完成之后,继续执行;

在loadFromDisk()方法中,如果加载成功会把mLoaded标志位置为true,然后 mLock.notifyAll();

最终,就把位于磁盘中的文件,加载到了内存中对应一个SharedPreferces对象,SharedPreferences中mMap。

2、编辑提交

当想SP中存入数据的时候,实例代码如下。

sharedPreferences.edit().putInt("number", 100).puString("age","18").apply();
sharedPreferences.edit().putInt("number", 100).commit();
复制代码

调用sharedPreferences.edit()返回一个EditorImpl对象,操作数据之后调用apply()或者commit()。

2.1、 commit()流程

   @Override
    public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();//写入内存
    SharedPreferencesImpl.this.enqueueDiskWrite(//写入磁盘
          mcr, null /* sync write on this thread okay */);
        try {
            mcr.writtenToDiskLatch.await();//等待写入磁盘执行完毕
        } catch (InterruptedException e) {
                return false;
        } finally {}
        notifyListeners(mcr);//通知监听
        return mcr.writeToDiskResult;
      }
      
     //
    private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
        //如果postWriteRunnable为空表示来自commit()方法调用
        final boolean isFromSyncCommit = (postWriteRunnable == null);
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);//写入磁盘
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
            //当commit提交,且mDiskWritesInFlight为1的时候,直接在当前所在线程执行写入磁盘操作
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        
        //交个QueuedWork,QueuedWork内部维护了一个HandlerThread,一直执行写入磁盘操作。
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

复制代码

 

每日一问:谈谈 SharedPreferences 的 apply() 和 commit()_第2张图片

 

 

如注释:当调用commit()方法之后

  • 首先将编辑的结果同步到内存中。

  • enqueueDiskWrite()将这个结果同步到磁盘中,enqueueDiskWrite()的第二个参数postWriteRunnable传入空。通常情况下也就是mDiskWritesInFlight(正在执行的写入磁盘操作的数量)为1的时候,直接在当前所在线程执行写入磁盘操作。否则还是异步到QueuedWork中去执行。commit()时,写入磁盘操作会发生在当前线程的说法是不准确的

  • 执行mcr.writtenToDiskLatch.await(); MemoryCommitResult 中有个一个CountDownLatch 成员变量,他的具体作用可以查阅其他资料。总的来说,当前线程执行会堵塞在这,直到mcr.writtenToDiskLatch满足了条件。也就是当写入磁盘成功之后,会继续执行下面的操作。

  • 所以,commit提交之后会有返回结果,同步堵塞直到有返回结果

2.2、 apply()流程

   @Override
        public void apply() {
            final long startTime = System.currentTimeMillis();
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {mcr.writtenToDiskLatch.await();}
                   };
            QueuedWork.addFinisher(awaitCommit);
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }
复制代码
  • 加入到QueuedWork中,是一个单线程的操作。
  • 没有返回结果。
  • 默认会有100ms的延迟

2.3 、QueuedWork

2.3.1、 关于延迟磁盘写入。

    /** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
    private static final long DELAY = 100;
    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();
        synchronized (sLock) {
            sWork.add(work);
            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
复制代码
  • 当apply()方式提交的时候,默认消息会延迟发送100毫秒,避免频繁的磁盘写入操作。
  • 当commit()方式,调用QueuedWork的queue()时,会立即向handler()发送Message。

2.3.2、主线程堵塞ANR

You don't need to worry about Android component lifecycles and their interaction with apply() writing to disk. The framework makes sure in-flight disk writes from apply() complete before switching states.

官方文档中有这样段化话,意思是您不需要担心Android组件生命周期及其对apply()写入磁盘的影响。框层架确保在切换状态之前完成使用apply()方法正在执行磁盘写入的动作。

然而还真是不让人那么省心。

罪魁祸首在这:

//QueuedWork.java
    public static void waitToFinish() {
        ...
          processPendingWork();//执行文件写入磁盘操作
        ....
    }
    private static void processPendingWork() {
        long startTime = 0;
      ....
     if (work.size() > 0) {
         for (Runnable w : work) {
             w.run();
         }
      ...  
    }
复制代码

waitToFinish()会将,储存在QueuedWork的操作一并处理掉。什么时候呢?在Activiy的 onPause()、BroadcastReceiver的onReceive()以及Service的onStartCommand()方法之前都会调用waitToFinish()。大家知道这些方法都是执行在主线程中,一旦waitToFinish()执行超时,就会跑出ANR。

至于waitToFinish调用具体时机,查看ActivityThread.java类文件。这里只是说本质原理。

3、跨进程操作的解决方案

\\ContextImpl
private void checkMode(int mode) {
        if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
            if ((mode & MODE_WORLD_READABLE) != 0) {
                throw new SecurityException("MODE_WORLD_READABLE no longer supported");
            }
            if ((mode & MODE_WORLD_WRITEABLE) != 0) {
                throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
            }
        }
    }
复制代码

Andorid 7.0及以上会抛出异常,Sharepreferences不再支持多进程模式。多进程共享文件会出现问题的本质在于,因为不同进程,所以线程同步会失效。要解决这个问题,可尝试跨进程解决方案,如ContentProvider、AIDL、AIDL、Service。

4、替代方案

  • 有问题,主线程堵塞。
  • 效率低。
  • 一不留神容易产生ANR。

既然SharedPreferences有这么多问题?就没人管管吗? 温和的治理方法或者说小建议

4.1、 温和改良派

  • 低频 尽量保证多次edit一个apply,原因上文讲过,尽量维持低频的写入。
  • 异步 能用apply()方法提交的就用apply()方法提交,原因这个方法是异步的,有延迟的(100s)
  • 小量 尽量维持Sharepreferences的体量小些,方便磁盘快速写入。
  • 合规 如果村JSON数据,就不要使用Sharepreferences了,因为SharedPerences本质是xml文件格式存储的,要存储JSON文件需要转义效率很低。不如直接自己编写代码文件读写在App私有目录中存储。

4.2、 激进铲除派

  • 腾讯微信团队的MMKV采用内存映射的方式,解决SharedPreferences的各种问题。
  • 原理基于内存映射mmap,具体使用 原理 源码 github.com/Tencent/MMK…

5、 小结

通过本文我们了解了SharedPreferences的基本原理。再回头看看文章开头的那几个问题,是不是有答案了。

  • commit()方法和apply()方法的区别:commit()方法是同步的有返回结果,同步保证使用Countdownlatch,即使同步但不保证往磁盘的写入是发生在当前线程的。apply()方法是异步的具体发生在QueuedWork中,里面维护了一个单线程去执行磁盘写入操作。
  • commit()和apply()方法其实都是Block主线程。commit()只要在主线程调用就会堵塞主线程;apply()方法磁盘写入操作虽然是异步的,但是当组件(Activity Service BroadCastReceiver)这些系统组件特定状态转换的时候,会把QueuedWork中未完成的那些磁盘写入操作放在主线程执行,且如果比较耗时会产生ANR,手动可怕。
  • 跨进程操作,需要借助Android平台常规的IPC手段(如,AIDL ContentProvider等)来完成。
  • 替代解决方案:看4。

彻底搞懂 SharedPreferences

我们知道 SharedPreferences 会从文件读取 xml 文件, 并将其以 getXxx/putXxx 的形式提供读写服务. 其中涉及到如下几个问题:
1. 如何从磁盘读取配置到内存
2. getXxx 如何从内存中获取配置
3. 最终配置如何从内存回写到磁盘
4. 多线程/多进程是否会有问题
5. 最佳实践

结论

  • SharedPreferences 是线程安全的. 内部由大量 synchronized 关键字保障
  • SharedPreferences 不是进程安全的
  • 第一次 getSharedPreferences 会读取磁盘文件, 后续的 getSharedPreferences 会从内存缓存中获取. 如果第一次调用 getSharedPreferences 时还没从磁盘加载完毕就调用 getXxx/putXxx , 则 getXxx/putXxx 操作会卡主, 直到数据从磁盘加载完毕后返回
  • 所有的 getXxx 都是从内存中取的数据
  • apply 是同步回写内存, 然后把异步回写磁盘的任务放到一个单线程的队列中等待调度. commit 和前者一样, 只不过要等待异步磁盘任务结束后才返回
  • MODE_MULTI_PROCESS 是在每次 getSharedPreferences 时检查磁盘上配置文件上次修改时间和文件大小, 一旦所有修改则会重新从磁盘加载文件. 所以并不能保证多进程数据的实时同步
  • 从 Android N 开始, 不再支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE. 一旦指定, 会抛异常

最佳实践

  • 不要多进程使用, 很小几率会造成数据全部丢失, 现象是配置文件被删除
  • 不要依赖 MODE_MULTI_PROCESS. 这个标记就像 MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE 未来会被废弃
  • 每次 apply / commit 都会把全部的数据一次性写入磁盘, 所以单个的配置文件不应该过大, 影响整体性能

源码全面分析

现在我们从源码入手, 彻底搞懂 SharedPreferences 有关的所有问题. 本文并不是入门教程, 主要面向有 SharedPreferences 基础的同学

SharedPreferences 对象的获取

一般来说有如下方式:
1. PreferenceManager.getDefaultSharedPreferences
2. ContextImpl.getSharedPreferences

我们以上述 [1] 为例来看看源码:

// PreferenceManager.java
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());
}

上述无论那种方法, 最终都是调用到了 ContextImpl.getSharedPreferences. 源码:

// ContextImpl.java
public SharedPreferences getSharedPreferences(File file, int mode) {
    checkMode(mode);
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            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.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

可见 sdk 是先取了缓存, 如果缓存未命中, 才构造对象. 也就是说, 多次 getSharedPreferences 几乎是没有代价的. 同时, 实例的构造被 synchronized 关键字包裹, 因此构造过程是多线程安全的

SharedPreferences 的构造

我们再来看看第一次构建对象时发生了什么:

// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}

有这么几个关键信息:
1. mFile 代表我们磁盘上的配置文件
2. mBackupFile 是一个灾备文件, 用户写入失败时进行恢复, 后面会再说. 其路径是 mFile 加后缀 ‘.bak’
3. mMap 用于在内存中缓存我们的配置数据, 也就是 getXxx 数据的来源

还涉及到一个 startLoadFromDisk, 我们来看看:

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

开启了一个线程从文件读取, 其源码如下:

// SharedPreferencesImpl.java
private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    ... 略去无关代码 ...

    str = new BufferedInputStream(
            new FileInputStream(mFile), 16*1024);
    map = XmlUtils.readMapXml(str);

    synchronized (SharedPreferencesImpl.this) {
        mLoaded = true;
        if (map != null) {
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }
        notifyAll();
    }
}

loadFromDisk 这个函数很关键. 它就是实际从磁盘读取配置文件的函数. 可见, 它做了如下几件事:
1. 如果有 ‘灾备’ 文件, 则直接使用灾备文件回滚.
2. 把配置从磁盘读取到内存的并保存在 mMap 字段中(看代码最后 mMap = map)
3. 标记读取完成, 这个字段后面 awaitLoadedLocked 会用到. 记录读取文件的时间, 后面 MODE_MULTI_PROCESS 中会用到
4. 发一个 notifyAll 通知已经读取完毕, 激活所有等待加载的其他线程

总结一下(点击大图):

 

getXxx 的流程

// SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

可见, 所有的 get 操作都是线程安全的. 并且 get 仅仅是从内存中(mMap) 获取数据, 所以无性能问题.

考虑到 配置文件的加载 是在单独的线程中异步进行的(参考 ‘SharedPreferences 的构造’), 所以这里的 awaitLoadedLocked 是在等待配置文件加载完毕. 也就是说如果我们第一次构造 SharedPreferences 后就立刻调用 getXxx 方法, 很有可能读取配置文件的线程还未完成, 所以这里要等待该线程做完相应的加载工作. 来看看 awaitLoadedLocked 的源码:

// SharedPreferencesImpl.java 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();
    } while (!mLoaded) { try { wait(); } catch (InterruptedException unused) { } } } 

很明显, 如果加载还未完成(mLoaded == false), getXxx 会卡在 awaitLoadedLocked, 一旦加载配置文件的线程工作完毕, 则这个加载线程会通过 notifyAll 会通知所有在 awaitLoadedLocked 中等待的线程, getXxx 就能够返回了. 不过大部分情况下, mLoaded == true. 这样的话 awaitLoadedLocked 会直接返回

putXxx 的流程

set 比 get 稍微麻烦一点儿, 因为涉及到 Editor 和 MemoryCommitResult 对象

先来看看 edit() 方法的实现:

// SharedPreferencesImpl.java
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 (this) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

Editor

Editor 没有构造函数, 只有两个属性被初始化:

// SharedPreferencesImpl.java
public final class EditorImpl implements Editor {
    private final Map<String, Object> mModified = Maps.newHashMap();
    private boolean mClear = false;

    ... 略去方法定义 ...
    public Editor putString(String key, @Nullable String value) { ... }
    public boolean commit() { ... }
    ...
}
  • mModified 是我们每次 putXxx 后所改变的配置项
  • mClear 标识要清空配置项, 但是只清了 SharedPreferences.mMap. 所以不要写这样的愚蠢代码:
sharedPreferences.edit()
        .putBoolean("foo", true)        // foo 无法被 clear 掉
        .clear()
        .putBoolean("bar", true)
        .commit()

edit() 会保障配置已从磁盘读取完毕, 然后仅仅创建了一个对象. 接下来看看 putXxx 的真身:

// SharedPreferencesImpl.java
public Editor putString(String key, @Nullable String value) {
    synchronized (this) {
        mModified.put(key, value);
        return this;
    }
}

很简单, 仅仅是把我们设置的配置项放到了 mModified 属性里保存. 等到 apply 或者 commit 的时候回写到内存和磁盘. 咱们分别来看看

apply

apply 是各种 ‘最佳实践’ 推荐的方式, 那么它到底是怎么异步工作的呢? 我们来看个究竟:

// SharedPreferencesImpl.java
public void apply() {
    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);
            }
        };

    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);
}

可以看出大致的脉络:
1. commitToMemory 应该是把修改的配置项回写到内存
2. QueuedWork.add(awaitCommit) 貌似没什么卵用
3. SharedPreferencesImpl.this.enqueueDiskWrite 把配置项加入到一个异步队列中, 等待调度

我们来看看 commitToMemory 的实现(略去大量无关代码):

// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {

        ... 略去无关 ...

        mcr.mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        synchronized (this) {
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // "this" is the magic value for a removal mutation. In addition,
                // setting a value to "null" for a given key is specified to be
                // equivalent to calling remove on that key.
                if (v == this || v == null) {
                    mMap.remove(k);
                } else {
                    mMap.put(k, v);
                }
            }

            mModified.clear();
        }
    }
    return mcr;
}

总结来说就两件事:
1. 把 Editor.mModified 中的配置项回写到 SharedPreferences.mMap 中, 完成了内存的同步
2. 把 SharedPreferences.mMap 保存在了 mcr.mapToWriteToDisk 中. 而后者就是即将要回写到磁盘的数据源

我们再来回头看看 apply 方法:

// SharedPreferencesImpl.java
public void apply() {
    final MemoryCommitResult mcr = commitToMemory();

    ... 略无关 ...

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
  • commitToMemory 完成了内存的同步回写
  • enqueueDiskWrite 完成了硬盘的异步回写, 我们接下来具体看看

enqueueDiskWrite

// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr);
                }

                ...
            }
        };

    ...

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

QueuedWork.singleThreadExecutor 实际上就是 ‘一个线程的线程池’, 如下:

// QueuedWork.java
public static ExecutorService singleThreadExecutor() {
    synchronized (QueuedWork.class) {
        if (sSingleThreadExecutor == null) {
            // TODO: can we give this single thread a thread name?
            sSingleThreadExecutor = Executors.newSingleThreadExecutor();
        }
        return sSingleThreadExecutor;
    }
}

可以看出 google 工程师这里还留了一个 todo, 想要给这个专门负责向磁盘同步配置项的线程起一个名字. 这里坐等大神你给 google 提个 pr

回到 enqueueDiskWrite 中, 这里还有一个重要的函数叫做 writeToFile:

writeToFile

// SharedPreferencesImpl.java
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 (!mBackupFile.exists()) {
            if (!mFile.renameTo(mBackupFile)) {
                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);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        try {
            final StructStat stat = Os.stat(mFile.getPath());
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
        }
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        return;
    }

    // Clean up an unsuccessfully written file
    mFile.delete();
}

代码大致分为三个过程:
1. 先把已存在的老的配置文件重命名(加 ‘.bak’ 后缀), 然后删除老的配置文件. 这相当于做了灾备
2. 向 mFile 中一次性写入所有配置项. 即 mcr.mapToWriteToDisk(这就是 commitToMemory 所说的保存了所有配置项的字段) 一次性写入到磁盘. 如果写入成功则删除灾备文件, 同时记录了这次同步的时间
3. 如果上述过程 [2] 失败, 则删除这个半成品的配置文件

好了, 我们来总结一下 apply:
1. 通过 commitToMemory 将修改的配置项同步回写到内存 SharedPreferences.mMap 中. 此时, 任何的 getXxx 都可以获取到最新数据了
2. 通过 enqueueDiskWrite 调用 writeToFile 将所有配置项一次性异步回写到磁盘. 这是一个单线程的线程池

来看个时序图压压惊(点击大图):

每日一问:谈谈 SharedPreferences 的 apply() 和 commit()_第3张图片

commit

看过了 apply 再看 commit 就非常容易了.

// SharedPreferencesImpl.java
public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    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;
}

直接看时序图吧(点击大图):

每日一问:谈谈 SharedPreferences 的 apply() 和 commit()_第4张图片

只需关注最后一条 ‘等待异步任务返回’ 的线, 对比 apply 的时序图, 一眼就看出差别

registerOnSharedPreferenceChangeListener

最后需要提一下的就是 listener:
* 对于 apply, listener 回调时内存已经完成同步, 但是异步磁盘任务不保证是否完成
* 对于 commit, listener 回调时内存和磁盘都已经同步完毕

各种标记的作用

MODE_PRIVATE/MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE:

指的是, 在保存文件的时候设置的文件属性. PRIVATE 就只有自己和所属组的读写权限, READABLE/WRITEABLE 是对 other 用户和组的读写权限. 主要源码位于: FileUtils.setPermissions

MODE_MULTI_PROCESS:

阅读过本文的话你会知道, 一个 prefs 实例通常有两种获得途径, 一个是第一次被 new 创建出来的, 这种方式会实际的读取磁盘文件. 还一种是后续从缓存(sSharedPrefsCache) 中取出来了.

而这个标记的意思就是: 使用 getSharedPrefercences 获取实例时, 无论是从磁盘读文件构造对象还是从缓存获取, 都会检查实例的 ‘内存中保存的时间’ 和 ‘磁盘上文件的最后修改时间’, 如果内存中保存的时间和磁盘上文件的最后修改时间, 则重新加载文件. 可以认为如果实例是从磁盘读取构造出来的, 那么他的 ‘内存中保存的时间’ 和 ‘文件的最后修改时间’ 一定是一样的, 而从缓存中来的实例就不一样了, 因为它可能很早就被创建(那个时候就已经读取了磁盘的文件并记录了当时文件的最后修改时间), 在随后的期间里其他进程很可能修改过磁盘上的配置文件导致最后修改时间变化, 这时候当我们从缓存中再次获取这个实例的时候, 系统会帮你检查这个文件在这段时间是否被修改过(‘内存中保存的时间’ 和 ‘磁盘上文件的最后修改时间’ 是否一致), 如果被修改过, 则重新从磁盘读取配置文件, 保证获取实例的内容是最新的.

SharedPreferences 应该是任何一名 Android 初学者都知道的存储类了,它轻量,适合用于保存软件配置等参数。以键值对的 XML 文件形式存储在本地,程序卸载后也会一并清除,不会残留信息。

使用起来也非常简单。

 

// 读取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
// 写入
val editor = sharedPreferences.edit()
editor.putString("123","123")
editor.commit()

当我们写下这样的代码的时候,IDE 极易出现一个警告,提示我们用 apply() 来替换 commit()。原因也很简单,因为 commit() 是同步的,而 apply() 采用异步的方式通常来说效率会更高一些。但是,当我们把 editor.commit() 的返回值赋给一个变量的时候,这时候就会发现 IDE 没有了警告。这是因为 IDE 认为我们想要使用 editor.commit() 的返回值了,所以,通常来说,在我们不关心操作结果的时候,我们更倾向于使用 apply() 进行写入的操作。

获取 SharedPreferences 实例

我们可以通过 3 种方式来获取 SharedPreferences 的实例。
首先当然是我们最常见的写法。

 

getSharedPreferences("123", Context.MODE_PRIVATE)

Context 的任意子类都可以直接通过 getSharedPreferences() 方法获取到 SharedPreferences 的实例,接受两个参数,分别对应 XML 文件的名字和操作模式。其中 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 这两种模式已在 Android 4.2 版本中被废弃。

  • Context.MODE_PRIVATE: 指定该 SharedPreferences 数据只能被本应用程序读、写;
  • Context.MODE_WORLD_READABLE: 指定该 SharedPreferences 数据能被其他应用程序读,但不能写;
  • Context.MODE_WORLD_WRITEABLE: 指定该 SharedPreferences 数据能被其他应用程序读写;
  • Context.MODE_APPEND:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件;

另外在 Activity 的实现中,还可以直接通过 getPreferences() 获取,实际上也就把当前 Activity 的类名作为文件名参数。

 

public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}   

此外,我们也可以通过 PreferenceManagergetDefaultSharedPreferences() 获取到。

 

public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());
}

public static String getDefaultSharedPreferencesName(Context context) {
    return context.getPackageName() + "_preferences";
}

private static int getDefaultSharedPreferencesMode() {
    return Context.MODE_PRIVATE;
}

可以很明显的看到,这个方式就是在直接把当前应用的包名作为前缀来进行命名的。

注意:如果在 Fragment 中使用 SharedPreferences 时,SharedPreferences 的初始化尽量放在 onAttach(Activity activity) 里面进行 ,否则可能会报空指针,即 getActivity() 会可能返回为空。

SharedPreferences 源码(基于 API 28)

有较多 SharedPreferences 使用经验的人,就会发现 SharedPreferences 其实具备挺多的坑,但这些坑主要都是因为不熟悉其中真正的原理所导致的,所以,笔者在这里,带大家一起揭开 SharedPreferences 的神秘面纱。

SharedPreferences 实例获取

前面讲了 SharedPreferences 有三种获取实例的方法,但归根结底都是调用的 ContextgetSharedPreferences() 方法。由于 Android 的 Context 类采用的是装饰者模式,而装饰者对象其实就是 ContextImpl,所以我们来看看源码是怎么实现的。

 

// 存放的是名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name
private ArrayMap mSharedPrefsPaths;

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) {
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}

@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}

private File makeFilename(File base, String name) {
    if (name.indexOf(File.separatorChar) < 0) {
        return new File(base, name);
    }
    throw new IllegalArgumentException(
            "File " + name + " contains a path separator");
}

可以很明显的看到,内部是采用 ArrayMap 来做的处理,而这个 mSharedPrefsPaths 主要是用于存放名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name,这时候我们通过 name 拿到我们的 File,如果当前池子中没有的话,则直接新建一个 File,并放入到 mSharedPrefsPaths 中。最后还是调用的重载方法 getSharedPreferences(File,mode)

 

// 存放包名与ArrayMap键值对,初始化时会默认以包名作为键值对中的 Key,注意这是个 static 变量
private static ArrayMap> sSharedPrefsCache;

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            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");
                }
            }
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            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.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}   

private ArrayMap getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

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

    return packagePrefs;
}

可以看到,又采用了一个 ArrayMap 来存放文件和 SharedPreferencesImpl 组成的键值对,然后通过通过单例的方式返回一个 SharedPreferences 对象,实际上是 SharedPreferences 的实现类 SharedPreferencesImpl,而且在其中还建立了一个内部缓存机制。

所以,从上面的分析中,我们能知道 对于一个相同的 name,我们获取到的都是同一个 SharedPreferencesImpl 对象。

SharedPreferencesImpl

在上面的操作中,我们可以看到在第一次调用 getSharedPreferences 的时候,我们会去构造一个 SharedPreferencesImpl 对象,我们来看看都做了什么。

 

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;
        }
        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;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
    }
}

注意看我们的 startLoadFromDisk 方法,我们会去新开一个子线程,然后去通过 XmlUtils.readMapXml() 方法把指定的 SharedPreferences 文件的所有的键值对都读出来,然后存放到一个 map 中。

而众所周知,文件的读写操作都是耗时的,可想而知,在我们第一次去读取一个 SharedPreferences 文件的时候花上了太多的时间会怎样。

SharedPreferences 的读取操作

上面讲了初次获取一个文件的 SharedPreferences 实例的时候,会先去把所有键值对读取到缓存中,这明显是一个耗时操作,而我们正常的去读取数据的时候,都是类似这样的代码。

 

val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")

SharedPreferencesgetXXX() 方法可能会报 ClassCastException 异常,所以我们在同一个 name 的时候,对不一样的类型,必须使用不同的 key。但是 putXXX 是可以用不同的类型值覆盖相同的 key 的。

那势必可能会导致这个操作需要等待一定的时间,我们姑且可以这么猜想,在 getXXX() 方法执行的时候应该是会等待前面的操作完成才能执行的。

因为 SharedPreferences 是一个接口,所以我们主要来看看它的实现类 SharedPreferencesImpl,这里以 getString() 为例。

 

@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

awaitLoadedLocked() 方法应该就是我们所想的等待执行操作了,我们看看里面做了什么。

 

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();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}   

可以看到,在 awaitLoadedLocked 方法里面我们使用了 mLock.wait() 来等待初始化的读取操作,而我们前面看到的 loadFromDiskLocked() 方法的最后也可以看到它调用了 mLock.notifyAll() 方法来唤醒后面这个阻塞的 getXXX()那么这里就会明显出现一个问题,我们的 getXXX() 方法是写在 UI 线程的,如果这个方法被阻塞的太久,势必会出现 ANR 的情况。所以我们一定在平时需要根据具体情况考虑是否需要把 SharedPreferences 的读写操作放在子线程中。

SharedPreferences 的内部类 Editor

我们在写入数据之前,总是要先通过类似这样的代码获取 SharedPreferences 的内部类 Editor

 

val editor = sharedPreferences.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();
    }

我们在

可以看到,我们在读取解析完 XML 文件的时候,直接返回了一个 Editor 的实现类 EditorImpl。我们随便查看一个 putXXX 的方法一看。

 

private final Object mEditorLock = new Object();

@GuardedBy("mEditorLock")
private final Map mModified = new HashMap<>();

@GuardedBy("mEditorLock")
private boolean mClear = false;

@Override
public Editor putString(String key, @Nullable String value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

可以看到,我们在 EditorImpl 里面使用了一个 HashMap 来存放我们的键值对数据,每次 put 的时候都会直接往这个键值对变量 mModified 中进行数据的 put 操作。

commit() 和 apply()

我们总是在更新数据后需要加上 commit() 或者 apply() 来进行输入的写入操作,我们不妨来看看他们的实现到底有什么区别。

先看 commit() 和 apply() 的源码。

 

@Override
public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    MemoryCommitResult mcr = commitToMemory();

    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

@Override
public void apply() {
    final long startTime = System.currentTimeMillis();

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    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()commit() 的区别是在 commit() 把内容同步提交到了硬盘,而 apply() 是先立即把修改提交给了内存,然后开启了一个异步的线程提交到硬盘。commit() 会接收 MemoryCommitResult 里面的一个 boolean 参数作为结果,而 apply() 没有对结果做任何关心。

我们可以看到,文件写入更新的操作都是交给 commitToMemory() 做的,这个方法返回了一个 MemoryCommitResult 对象,我们来看看到底做了什么。

 

// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    List keysModified = null;
    Set listeners = null;
    Map mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // 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(mMap);
        }
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList();
            listeners = new HashSet(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }

            for (Map.Entry e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // "this" is the magic value for a removal mutation. In addition,
                // setting a value to "null" for a given key is specified to be
                // equivalent to calling remove on that key.
                if (v == 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();

            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }

            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}

可以看到,我们这里的 mMap 即存放当前 SharedPreferences 文件中的键值对,而 mModified 则存放的是当时 edit() 时 put 进去的键值对,这个我们前面有所介绍。这里有个 mDiskWritesInFlight 看起来应该是表示正在等待写的操作数量。

接下来我们首先处理了 edit().clear() 操作的 mClear 标志,当我们在外面调用 clear() 方法的时候,我们会把 mClear 设置为 true,这时候我们会直接通过 mMap.clear() 清空此时文件中的键值对,然后再遍历 mModified 中新 put 进来的键值对数据放到 mMap 中。也就是说:在一次提交中,如果我们又有 put 又有 clear() 操作的话,我们只能 clear() 掉之前的键值对,这次 put() 进去的键值对还是会被写入到 XML 文件中。

 

// 读取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
// 写入
val editor = sharedPreferences.edit()
editor.putInt("1", 123)
editor.clear()
editor.apply()
Log.e("nanchen2251", "${sharedPreferences.getInt("1", 0)}")

也就是说,当我们编写下面的代码的时候,得到的打印还是 123。

然后我们接着往下看,又发现了另外一个 commit()apply() 都做了调用的方法是 enqueueDiskWrite()

 

/**
 * Enqueue an already-committed-to-memory result to be written
 * to disk.
 *
 * They will be written to disk one-at-a-time in the order
 * that they're enqueued.
 *
 * @param postWriteRunnable if non-null, we're being called
 *   from apply() and this is the runnable to run after
 *   the write proceeds.  if null (from a regular commit()),
 *   then we're allowed to do this disk write on the main
 *   thread (which in addition to reducing allocations and
 *   creating a background thread, this has the advantage that
 *   we catch them in userdebug StrictMode reports to convert
 *   them where possible to apply() ...)
 */
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

在这个方法中,首先通过判断 postWriteRunnable 是否为 null 来判断是 apply() 还是 commit()。然后定义了一个 Runnable 任务,在 Runnable 中先调用了 writeToFile() 进行了写入和计数器更新的操作。

然后我们再来看看这个 writeToFile() 方法做了些什么。

 

@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    long startTime = 0;
    long existsTime = 0;
    long backupExistsTime = 0;
    long outputStreamCreateTime = 0;
    long writeTime = 0;
    long fsyncTime = 0;
    long setPermTime = 0;
    long fstatTime = 0;
    long deleteTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    boolean fileExists = mFile.exists();

    if (DEBUG) {
        existsTime = System.currentTimeMillis();

        // Might not be set, hence init them to a default value
        backupExistsTime = existsTime;
    }

    // Rename the current file so it may be used as a backup during the next read
    if (fileExists) {
        boolean needsWrite = false;

        // Only need to write if the disk state is older than this commit
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    // No need to persist intermediate states. Just wait for the latest state to
                    // be persisted.
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }

        if (!needsWrite) {
            mcr.setDiskWriteResult(false, true);
            return;
        }

        boolean backupFileExists = mBackupFile.exists();

        if (DEBUG) {
            backupExistsTime = System.currentTimeMillis();
        }
        // 此处需要注意一下
        if (!backupFileExists) {
            if (!mFile.renameTo(mBackupFile)) {
                Log.e(TAG, "Couldn't rename file " + mFile
                      + " to backup file " + mBackupFile);
                mcr.setDiskWriteResult(false, 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 (DEBUG) {
            outputStreamCreateTime = System.currentTimeMillis();
        }

        if (str == null) {
            mcr.setDiskWriteResult(false, false);
            return;
        }
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        writeTime = System.currentTimeMillis();

        FileUtils.sync(str);

        fsyncTime = System.currentTimeMillis();

        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

        if (DEBUG) {
            setPermTime = System.currentTimeMillis();
        }

        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (mLock) {
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }

        if (DEBUG) {
            fstatTime = System.currentTimeMillis();
        }

        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();

        if (DEBUG) {
            deleteTime = System.currentTimeMillis();
        }

        mDiskStateGeneration = mcr.memoryStateGeneration;

        mcr.setDiskWriteResult(true, true);

        if (DEBUG) {
            Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                    + (backupExistsTime - startTime) + "/"
                    + (outputStreamCreateTime - startTime) + "/"
                    + (writeTime - startTime) + "/"
                    + (fsyncTime - startTime) + "/"
                    + (setPermTime - startTime) + "/"
                    + (fstatTime - startTime) + "/"
                    + (deleteTime - startTime));
        }

        long fsyncDuration = fsyncTime - writeTime;
        mSyncTimes.add((int) fsyncDuration);
        mNumSync++;

        if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
            mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
        }

        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, false);
}

代码比较长,做了一些时间的记录和 XML 的相关处理,但最值得我们关注的还是其中打了标注的对于 mBackupFile 的处理。我们可以明显地看到,在我们写入文件的时候,我们会把此前的 XML 文件改名为一个备份文件,然后再将要写入的数据写入到一个新的文件中。如果这个过程执行成功的话,就会把备份文件删除。由此可见:即使我们每次只是添加一个键值对,也会重新写入整个文件的数据,这也说明了 SharedPreferences 只适合保存少量数据,文件太大会有性能问题。

看完了这个 writeToFile() ,我们再来看看下面做了啥。

 

// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
    boolean wasEmpty = false;
    synchronized (mLock) {
        wasEmpty = mDiskWritesInFlight == 1;
    }
    if (wasEmpty) {
        writeToDiskRunnable.run();
        return;
    }
}

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);       

可以看到,当且仅当是 commit() 并且只有一个待写入操作的时候才能直接执行到 writeToDiskRunnable.run(),否则都会执行到 QueuedWorkqueue() 方法,这个 QueuedWork 又是什么东西?

 

/** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
@GuardedBy("sLock")
private static final LinkedList sFinishers = new LinkedList<>();

/** Work queued via {@link #queue} */
@GuardedBy("sLock")
private static final LinkedList sWork = new LinkedList<>();
/**
 * Internal utility class to keep track of process-global work that's outstanding and hasn't been
 * finished yet.
 *
 * New work will be {@link #queue queued}.
 *
 * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
 * This is used to make sure the work has been finished.
 *
 * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
 * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
 * other things in the future.
 *
 * The queued asynchronous work is performed on a separate, dedicated thread.
 *
 * @hide
 */
public class QueuedWork {
     /**
     * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
     *
     * Used by SharedPreferences$Editor#startCommit().
     *
     * Note that this doesn't actually start it running.  This is just a scratch set for callers
     * doing async work to keep updated with what's in-flight. In the common case, caller code
     * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
     * these Runnables are run is from {@link #waitToFinish}.
     *
     * @param finisher The runnable to add as finisher
     */
    public static void addFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.add(finisher);
        }
    }

    /**
     * Remove a previously {@link #addFinisher added} finisher-runnable.
     *
     * @param finisher The runnable to remove.
     */
    public static void removeFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.remove(finisher);
        }
    }

    /**
     * Trigger queued work to be processed immediately. The queued work is processed on a separate
     * thread asynchronous. While doing that run and process all finishers on this thread. The
     * finishers can be implemented in a way to check weather the queued work is finished.
     *
     * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
     * after Service command handling, etc. (so async work is never lost)
     */
    public static void waitToFinish() {
        long startTime = System.currentTimeMillis();
        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);

                if (DEBUG) {
                    hadMessages = true;
                    Log.d(LOG_TAG, "waiting");
                }
            }

            // We should not delay any work as this might delay the finishers
            sCanDelay = false;
        }

        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
            processPendingWork();
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }

        try {
            while (true) {
                Runnable finisher;

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

                if (finisher == null) {
                    break;
                }

                finisher.run();
            }
        } finally {
            sCanDelay = true;
        }

        synchronized (sLock) {
            long waitTime = System.currentTimeMillis() - startTime;

            if (waitTime > 0 || hadMessages) {
                mWaitTimes.add(Long.valueOf(waitTime).intValue());
                mNumWaits++;

                if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                    mWaitTimes.log(LOG_TAG, "waited: ");
                }
            }
        }
    }
    /**
     * Queue a work-runnable for processing asynchronously.
     *
     * @param work The new runnable to process
     * @param shouldDelay If the message should be delayed
     */
    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }
}

简单地说,这个 QueuedWork 类里面有一个专门存放 Runnable 的两个 LinkedList 对象,他们分别对应未完成的操作 sFinishers 和正在工作的 sWork
我们在 waitToFinish() 方法中,会不断地去遍历执行未完成的 Runnable。我们根据注释也知道了这个方法会在 ActivityonPause()BroadcastReceiveronReceive() 方法后调用。假设我们频繁的调用了 apply()方法,并紧接着调用了 onPause() ,那么就可能会发生 onPause() 一直等待 QueuedWork.waitToFinish 执行完成而产生 ANR。也就是说,即使是调用了 apply() 方法去异步提交,也不是完全安全的。如果 apply() 方法使用不当,也是可能出现 ANR 的。

总结

说了这么多,我们当然还是需要做一个总结。

  1. apply() 没有返回值而 commit() 返回 boolean 表明修改是否提交成功 ;
  2. commit() 是把内容同步提交到硬盘的,而 apply() 先立即把修改提交到内存,然后开启一个异步的线程提交到硬盘,并且如果提交失败,你不会收到任何通知。
  3. 所有 commit() 提交是同步过程,效率会比 apply() 异步提交的速度慢,在不关心提交结果是否成功的情况下,优先考虑 apply() 方法。
  4. apply() 是使用异步线程写入磁盘,commit() 是同步写入磁盘。所以我们在主线程使用的 commit() 的时候,需要考虑是否会出现 ANR 问题。
  5. 我们每次添加键值对的时候,都会重新写入整个文件的数据,所以它不适合大量数据存储。
  6. 多线程场景下效率比较低,因为 get 操作的时候,会锁定 SharedPreferencesImpl 里面的对象,互斥其他操作,而当 putcommit()apply() 操作的时候都会锁住 Editor 的对象,在这样的情况下,效率会降低。
  7. 由于每次都会把整个文件加载到内存中,因此,如果 SharedPreferences 文件过大,或者在其中的键值对是大对象的 JSON 数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁 GC,导致的界面卡顿。
  8. get 操作都是线程安全的, 并且 get 仅仅是从内存中 (mMap) 获取数据, 所以无性能问题。
    基于以上缺点:
  9. 建议不要存储较大数据到 SharedPreferences,也不要把较多数据存储到同一个 name 对应的 SharedPreferences 中,最好根据规则拆分为多个 SharedPreferences 文件。
  10. 频繁修改的数据修改后统一提交,而不是修改过后马上提交。
  11. 在跨进程通讯中不去使用 SharedPreferences
  12. 获取 SharedPreferences 对象的时候会读取 SharedPreferences 文件,如果文件没有读取完,就执行了 get 和 put 操作,可能会出现需要等待的情况,因此最好提前获取 SharedPreferences 对象。
  13. 每次调用 edit() 方法都会创建一个新的 EditorImpl 对象,不要频繁调用 edit() 方法。
    参考链接:https://juejin.im/post/5adc444df265da0b886d00bc#heading-10

 

庖丁解牛之SharedPreferences超级大卡顿

背景

 最近在排查app卡顿问题,在公司内部的bug管理平台上发现这个类卡顿问题,知道卡顿了多长时间吗,足足4s多,这让线上用户怎么想?让我怎么想?

 

java.lang.Object.wait(Native Method)
java.lang.Thread.parkFor(Thread.java:1220)
sun.misc.Unsafe.park(Unsafe.java:299)
java.util.concurrent.locks.LockSupport.park(LockSupport.java:157)
java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:813)
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:973)
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1281)
java.util.concurrent.CountDownLatch.await(CountDownLatch.java:202)
android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:363)
android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3336)
android.app.ActivityThread.access$2300(ActivityThread.java:197)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1709)
android.os.Handler.dispatchMessage(Handler.java:111)
android.os.Looper.loop(Looper.java:224)
android.app.ActivityThread.main(ActivityThread.java:5958)
java.lang.reflect.Method.invoke(Native Method)
java.lang.reflect.Method.invoke(Method.java:372)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1113)

  刚开始以为是系统Unsafe的卡顿,就没怎么细看,后来发现不对中间居然看见了SharedPreferences的代码,之前就知道SharedPreferences这个玩意坑很多,我又回忆起之前面试一个面试者,他提到过如何采用objectbox替换SharedPreferences解决卡顿问题,我怀疑就是这玩意导致的,这激起了我的好奇心

问题原因分析

  项目中用了SharedPreferences 这个玩意,谁知道这额玩意有大坑呀,给我们app卡的不行不行的,代码在apply的时候,SharedPreferences 内部发送了一个异步任务取执行文件的写操作,按道理说写操作都是在异步线程中执行的,不应该会卡顿主线程呀,是的,读写操作时在异步线程,QueuedWork.waitToFinish 这个方法是在主线程中执行,具体的调用到代码在ActiviytThread 类的handleStopActivity 方法和handleServiceArgs 方法中等多处方法中有调用,我们出问题的地方就是调用了handleServiceArgs方法,QueuedWork.waitToFinish 这个方法中执行了线程操作,所以导致了主线程卡住了

commit 造成的卡顿

 我们先看一下SharedPreferencesImpl 这个类,这个类是具体的实现类,我们看一下commit方法

 

public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    MemoryCommitResult mcr = commitToMemory();

    //这地方是执行具体的写入任务
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        //看看没这个地方就让主线程卡住的原因
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

在commit方法中,首先执行写入任务也就是enqueueDiskWrite这个方法,我们稍后分析,然后让调用线程处于等待状态,当写入任务执行成功后唤起调用commit的线程,假设调用commit的线程就是主线线程,并且写入任务耗时还比较多的,这不就阻塞住主线程了吗?

 

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    //往系统的队列中发送任务,然后在工作线程中执行任务
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

 enqueueDiskWrite 方法中首先判断的postWriteRunnable 是否等于null,如果等于空了,就在当前调用的地方执行写入操作,如果不是就往QueuedWork 队列中发送任务

总结一下:如果是使用commit方式提交,会阻塞调用commit方法的线程,如果写入任务很多比较耗时,就卡住了,所以不要在主线程执行写入文件的操作,但是我们上线卡顿日志是另外一种情况,是使用了apply提交的时候才会出现的

apply造成的卡顿

 

public void apply() {
    final long startTime = System.currentTimeMillis();

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    //这个地方是造成卡顿的原因
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    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);
}

 enqueueDiskWrite是执行异步任务的方法,我们之前已经见过这个方法,在apply方法中调用enqueueDiskWrite方法的时候最后一个参数是不等于空的,也就是说我们要执行一个异步任务,最终这异步任务的执行是在QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)方法中

QueuedWork是干什么的呢?

 QueuedWork就是android系统提供的一个执行异步任务的工具类,内部的实现逻辑的就是创建一个HandlerThread作为工作线程,然后QueuedWorkHandler和这个HandlerThread进行管理,每当有任务添加进来就在这个异步线程中执行,这个异步线程的名字queued-work-looper

 

public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();

    synchronized (sLock) {
        sWork.add(work);

        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}

首先往sWork 添加一个任务,sWork是一个LinkedList,这个队列中数据最终在queued-work-looper 线程中依次得到执行

创建handle的过程

 

private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();

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

在QueuedWorkHandler 是如何处理消息的

 

    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() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    synchronized (sProcessingWork) {
        LinkedList work;

        synchronized (sLock) {
            work = (LinkedList) sWork.clone();
            sWork.clear();

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

        if (work.size() > 0) {
            for (Runnable w : work) {
                w.run();
            }

            if (DEBUG) {
                Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                        +(System.currentTimeMillis() - startTime) + " ms");
            }
        }
    }
}

实际上就是遍历sWork,挨个执行任务,

那为什么会出现上面的卡顿了?

 apply的中写入操作也是在异步线程执行,不会导致主线程卡顿,但是如果异步任务执行时间过长,当ActvityThread执行了handleStopActivity或者handleServiceArgs或者handlePauseActivity 等方法的时候都会调用QueuedWork.waitToFinish()方法,而此方法中会在异步任务执行完成前一直阻塞住主线程,所以卡顿问题就产生了

 

public static void waitToFinish() {
    long startTime = System.currentTimeMillis();
    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);

            if (DEBUG) {
                hadMessages = true;
                Log.d(LOG_TAG, "waiting");
            }
        }

        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }

    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
        processPendingWork();
    } finally {
        StrictMode.setThreadPolicy(oldPolicy);
    }

    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                //关键代码
                finisher = sFinishers.poll();
            }

            if (finisher == null) {
                break;
            }

            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }

    synchronized (sLock) {
        long waitTime = System.currentTimeMillis() - startTime;

        if (waitTime > 0 || hadMessages) {
            mWaitTimes.add(Long.valueOf(waitTime).intValue());
            mNumWaits++;

            if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                mWaitTimes.log(LOG_TAG, "waited: ");
            }
        }
    }
}

 会从sFinishers队列中取出数据然后执行run方法,我们别忘了在apply的方法中,我们还添加了QueuedWork.addFinisher(awaitCommit);这个awaitCommit 就得到执行了但是awaitCommit中的代码确实是阻塞的代码,等待写入线程执行完毕才能唤起此线程

 

final Runnable awaitCommit = new Runnable() {
        public void run() {
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {
            }

            if (DEBUG && mcr.wasWritten) {
                Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                        + " applied after " + (System.currentTimeMillis() - startTime)
                        + " ms");
            }
        }
    };

如果 apply中的写入代码不执行完,主线程就一直卡住了,也就出现了我们上面的问题

SharedPreferences 获取数据也是阻塞的

我们看一下SharedPreferences 的初始化代码

 

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}

数据加载是异步线程执行的

 

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

那为什么获取数据也是阻塞的?

看一下获取数据的get方法

 

public int getInt(String key, int defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        Integer v = (Integer)mMap.get(key);
        return v != null ? v : defValue;
    }
}

关键awaitLoadedLocked 这个方法,当数据没有加载完,就让调用的线程处于等待中,阻塞住了

 

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();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
}

总结

  • commit 方式会阻塞调用的线程
  • apply 放法不会阻塞调用的线程,但是如果写入任务比较耗时,会阻塞住主线程,因为主线程有调用的代码,需要等写入任务执行完了才会继续往下执行

建议

 SharePrefereces这玩意能不用就不用,坑还是比较隐晦的,如果不查看源代码是根本不可能知道的,一般大家用这个类主要是图简单省事 了,我就是存一个数据搞那么复杂干什么呢?我建议如果大家在线上的项目中存储数据还是用自己实现的方案吧,不要用这个了,如果非要用这个我建议开启一个线程然后在线程中调用commit方式更新数据,至少这个方案不会卡住主线程




链接:https://www.jianshu.com/p/40e42da910e2
 

你可能感兴趣的:(Android之路)