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);
}
}
}
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: ");
}
}
}
}
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();
}
}
}
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");
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
);
}
}
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 避开 QueuedWork
在 onPause()
, onDestroy()
等生命周期回调时在主线程中的同步操作。
基于 MMKV 的 SharedPreferences
此方案有考虑过,但是一直未推出,主要有以下几个方面的原因:
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
12MMKV 未校验数据类型,以下代码被认为是合法的
mmkv.edit().putInt("a", 1).apply(); boolean result = mmkv.getBoolean("a", false); // result 为 true
1
2
3MMKV 与系统原生 API 的行为不一致,如下面这段代码,MMKV 得到的结果与原生 API 的结果是不同的
editor.put("a", "abc").clear().apply();
1
所以,基于以上的考虑,Booster 暂时不会采用 MMKV 作为通用的优化方案,如果大家对这个方案感兴趣,而且以上问题都可以忽略的话,可以参考 booster-transform-shared-preferences在新窗口打开 和 booster-android-instrument-shared-preferences在新窗口打开 的实现。