SharedPreferences 优化

SharedPreferences 的问题

SharedPreferences在新窗口打开 的设计一直为人所诟病,其实,SharedPreferences在新窗口打开 从一开始被 Google 的工程师设计出来并不是像现在这样用的,只不过后来被大家玩儿坏了,以至于出现了各种卡顿和ANR。

SharedPreferences在新窗口打开 是如何造成卡顿和 ANR 的呢?这得从 SharedPreferences.Editor.apply()在新窗口打开 说起,如下图所示:

从图中可以看出,在 onPause() 的时候,会调用 QueuedWork.waitToFinish()在新窗口打开,而 waitToFinish() 就是将之前 Editor.apply()QueuedWork 队列中追加的 Runnable 在主线程同步执行,如下代码所示:

final class SharedPreferencesImpl implements SharedPreferences {

    public final class EditorImpl implements Editor {

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

            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                @Override
                public void run() {
                    try {
                        /*👇👇👇👇👇 最后发生 ANR 的位置 👇👇👇👇*/
                        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);
        }

    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    /**
     * 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: ");
                }
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

异步调用 commit()

booster v0.1.5在新窗口打开 推出的针对 SharedPreferences在新窗口打开 的优化,主要是将 Editor.apply()在新窗口打开 替换成 Editor.commit()在新窗口打开 并在子线程中执行,代码如下:

public class ShadowEditor {

    public static void apply(final SharedPreferences.Editor editor) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
                @Override
                public void run() {
                    editor.commit();
                }
            });
        } else {
            editor.commit();
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这个方案的优化点是改动很小,风险相对较小,缺点是有一种场景可能会有 bug —— 在调用 commit() 后立即调用 getXxx(),如下所示:

SharedPreferences sp = activity.getSharedPreferences(Context.MODE_PRIVATE);
sp.edit().putString("key", "value").commit();
String value = sp.getString("key");
1
2
3

注意

虽然这种写法本身就不可取,但是异步调用 commit() 确实会有一定的概率出现数据不同步

自定义 SharedPreferences

为了彻底解决 SharedPreferences 的问题,Booster v0.27.0在新窗口打开 推出了BoosterSharedPreferences在新窗口打开,通过 SharedPreferencesTransformer在新窗口打开 将所有调用 Context.getSharedPreferences(String, int) 的指令替换成 ShadowSharedPreferences.getSharedPreferences(Context, String, int),代码如下:

public class ShadowSharedPreferences {

    public static SharedPreferences getSharedPreferences(
        final Context context,
        final String name,
        final int mode
    ) {
        if (TextUtils.isEmpty(name)) {
            name = "null";
        }
        return BoosterSharedPreferences.getSharedPreferences(name);
    }

    public static SharedPreferences getPreferences(
        final Activity activity,
        final int mode
    ) {
        return getSharedPreferences(
            activity.getApplicationContext(),
            activity.getLocalClassName(),
            mode
        );
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

通过自定义 SharedPreferences 避开 QueuedWorkonPause(), onDestroy() 等生命周期回调时在主线程中的同步操作。

基于 MMKV 的 SharedPreferences

此方案有考虑过,但是一直未推出,主要有以下几个方面的原因:

  1. MMKV 未实现 OnSharedPreferenceChangeListener在新窗口打开 监听

    public class MMKV implements SharedPreferences, SharedPreferences.Editor {
    
        @Override
        public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
            throw new java.lang.UnsupportedOperationException("Not implement in MMKV");
        }
    
        @Override
        public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
            throw new java.lang.UnsupportedOperationException("Not implement in MMKV");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  2. MMKV 未校验数据类型,以下代码被认为是合法的

    mmkv.edit().putInt("a", 1).apply();
    boolean result = mmkv.getBoolean("a", false);
    // result 为 true
    
    1
    2
    3
  3. MMKV 与系统原生 API 的行为不一致,如下面这段代码,MMKV 得到的结果与原生 API 的结果是不同的

    editor.put("a", "abc").clear().apply();
    
    1

所以,基于以上的考虑,Booster 暂时不会采用 MMKV 作为通用的优化方案,如果大家对这个方案感兴趣,而且以上问题都可以忽略的话,可以参考 booster-transform-shared-preferences在新窗口打开booster-android-instrument-shared-preferences在新窗口打开 的实现。