这几天在Google Play的ANR实时报告中看到很多貌似与SharedPreferences相关的ANR,看了历史版本后发现,这个已经是一个老问题了,在历次版班的ANR中居高不下。今天实在忍不了,决定对这个问题一探究竟。
at java.lang.Object.wait! (Native method)
- waiting on <0x0a351954> (a java.lang.Object)
at java.lang.Thread.parkFor$ (Thread.java:1220)
- locked <0x0a351954> (a java.lang.Object)
at sun.misc.Unsafe.park (Unsafe.java:299)
at java.util.concurrent.locks.LockSupport.park (LockSupport.java:158)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt (AbstractQueuedSynchronizer.java:810)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly (AbstractQueuedSynchronizer.java:970)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly (AbstractQueuedSynchronizer.java:1278)
at java.util.concurrent.CountDownLatch.await (CountDownLatch.java:203)
at android.app.SharedPreferencesImpl$EditorImpl$1.run (SharedPreferencesImpl.java:366)
at android.app.QueuedWork.waitToFinish (QueuedWork.java:88)
at android.app.ActivityThread.handleServiceArgs (ActivityThread.java:3029)
at android.app.ActivityThread.access$2200 (ActivityThread.java:155)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1450)
at android.os.Handler.dispatchMessage (Handler.java:102)
at android.os.Looper.loop (Looper.java:175)
at android.app.ActivityThread.main (ActivityThread.java:5430)
at java.lang.reflect.Method.invoke! (Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:616)
这事Google Play提供的堆栈信息,信息非常有限,但是还是能观察到SharedPreferences的身影的,这个 waitToFinish
非常可疑,追踪到代码:
/**
* Finishes or waits for async operations to complete.
* (e.g. SharedPreferences$Editor#startCommit writes)
*
* 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() {
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}
这个方法的注释已经很清楚了,如果我们使用SharedPreference的apply方法, 虽然该方法可以很快返回, 并在其它线程里将键值对写入到文件系统, 但是当Activity, BroadcastReceiver和Service的一些回调方法被调用时,会等待写入到文件系统的任务完成,如果写入比较慢,主线程就会出现ANR问题。
另外,SharedPreference除了提供apply外还提供commit方法,源码如下所示,该方法直接在调用线程中执行,不会转入后台,但如果我们在UI线程commit,且磁盘写入较慢的情况下,ANR依然会发生。行文至此,问题应该描述的比较清楚了,可以理解为SharedPreferences的机制导致系统组件回调过程中的阻塞,触发了ANR。可以将这个问题视为Android一个bug,也可以将问题归咎于为使用SharedPreferences不当(在SharedPreferences里读写数据量太大), 但是问题终究要解决,所以下面提出解决方案,并且记录下解决过程中的实践经验。
- 方案一
考虑启动一个线程commit,不使用apply
- 方案二
Hook ActivityThread,拿到Handler变量,在调用QueuedWork.waitToFinish()之前,将其中保存的队列清空,防止ANR的发生
- 方案三
使用MMKV替代SharedPrferences
最后我们选用了第三种方案,对于我们现有的代码改动量最小。第一种方案可行,但是也有问题,会有线程同步,仍然需要管理其他线程,保证读写的一致性。第二种方案不好验证它的副作用,因此也没有采纳。
选用MMKV后也有一个问题需要解决,那就是数据迁移,因为不可能针对之前所有使用SharedPreferences的地方逐一进行迁移,因此写了一个MMKV的封装类来统一对外提供数据迁移和读写的接口,可以实现对现已代码最小改动的情况下完成替换和迁移。封装类如下:
public class SharedPreferencesStore {
public static final String TAG = SharedPreferencesStore.class.getSimpleName();
private MMKV mMMKV;
private SharedPreferencesStore(MMKV mmkv) {
mMMKV = mmkv;
}
// 初始化,必须在使用前调用
public static void initialize(Context context) {
String rootDir = MMKV.initialize(context);
ApplicationDelegate.log(TAG + " MMKV root directory=" + rootDir);
}
// 获取全局SharedPreferencesStore, 不需要迁移老的SharedPreferences数据的情况下调用
public static SharedPreferencesStore getGlobal() {
MMKV mmkv = MMKV.defaultMMKV();
return new SharedPreferencesStore(mmkv);
}
// 获取全局SharedPreferencesStore, 需要迁移老的SharedPreferences数据的情况下调用
public static SharedPreferencesStore getGlobalWithMigration(Context context) {
MMKV mmkv = MMKV.defaultMMKV();
importFromDefaultSharePreferences(mmkv, context);
return new SharedPreferencesStore(mmkv);
}
// 获取特定SharedPreferencesStore, 不需要迁移老的SharedPreferences数据的情况下调用
public static SharedPreferencesStore getByName(final String name) {
MMKV mmkv = MMKV.mmkvWithID(name);
return new SharedPreferencesStore(mmkv);
}
// 获取特定SharedPreferencesStore, 需要迁移老的SharedPreferences数据的情况下调用
public static SharedPreferencesStore getByNameWithMigration(Context context, final String name) {
MMKV mmkv = MMKV.mmkvWithID(name);
importFromSharePreferences(mmkv, context, name);
return new SharedPreferencesStore(mmkv);
}
// 需要多进程访问, 不需要迁移老的SharedPreferences数据的情况下调用
public static SharedPreferencesStore getMultiProcessByName(final String name) {
MMKV mmkv = MMKV.mmkvWithID(name, MMKV.MULTI_PROCESS_MODE);
return new SharedPreferencesStore(mmkv);
}
// 需要多进程访问, 需要迁移老的SharedPreferences数据的情况下调用
public static SharedPreferencesStore getMultiProcessByNameWithMigration(Context context, final String name) {
MMKV mmkv = MMKV.mmkvWithID(name, MMKV.MULTI_PROCESS_MODE);
importFromSharePreferencesMultiProcess(mmkv, context, name);
return new SharedPreferencesStore(mmkv);
}
public void put(final String key, boolean value) {
mMMKV.encode(key, value);
}
public boolean getBoolean(final String key, boolean defValue) {
return mMMKV.decodeBool(key, defValue);
}
public void put(final String key, int value) {
mMMKV.encode(key, value);
}
public int getInt(final String key, int defValue) {
return mMMKV.decodeInt(key, defValue);
}
public void put(final String key, long value) {
mMMKV.encode(key, value);
}
public long getLong(final String key, long defValue) {
return mMMKV.decodeLong(key, defValue);
}
public void put(final String key, float value) {
mMMKV.encode(key, value);
}
public float getFloat(final String key, float defValue) {
return mMMKV.decodeFloat(key, defValue);
}
public void put(final String key, double value) {
mMMKV.encode(key, value);
}
public double getDouble(final String key, double defValue) {
return mMMKV.decodeDouble(key, defValue);
}
public void put(final String key, final String value) {
mMMKV.encode(key, value);
}
public String getString(final String key, final String defValue) {
return mMMKV.decodeString(key, defValue);
}
public void put(final String key, final byte[] value) {
mMMKV.encode(key, value);
}
public byte[] getBytes(final String key) {
return mMMKV.decodeBytes(key);
}
public Map<String, ?> getAll() {
return mMMKV.getAll();
}
public void remove(final String key) {
mMMKV.remove(key);
}
public void removeForKey(final String key) {
mMMKV.removeValueForKey(key);
}
public void removeForKeys(final String[] keys) {
mMMKV.removeValuesForKeys(keys);
}
public boolean contains(final String key) {
return mMMKV.contains(key);
}
public boolean containsKey(final String key) {
return mMMKV.containsKey(key);
}
public SharedPreferences.Editor edit() {
return mMMKV.edit();
}
public static void importFromSharePreferences(MMKV mmkv, final Context context, final String name) {
if (null == context) {
return;
}
SharedPreferences sp = context.getSharedPreferences(name, Context.MODE_PRIVATE);
doImport(mmkv, sp, name);
}
public static void importFromSharePreferencesMultiProcess(MMKV mmkv, final Context context, final String name) {
if (context == null) {
return;
}
SharedPreferences sp = context.getSharedPreferences(name, Context.MODE_MULTI_PROCESS);
doImport(mmkv, sp, name);
}
public static void importFromDefaultSharePreferences(MMKV mmkv, final Context context) {
if (context == null) {
return;
}
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
doImport(mmkv, sp, context.getPackageName() + "_preferences");
}
private static void doImport(MMKV mmkv, SharedPreferences sp, final String name) {
ApplicationDelegate.log(TAG + " ------" + name + "-----SharedPreferences迁移开始-----------");
if (sp.getAll() == null || sp.getAll().size() == 0) {
ApplicationDelegate.log(TAG + " " + name + " SharedPreferences 为空,不需要迁移");
return;
}
int oldSize = sp.getAll().size();
ApplicationDelegate.log(TAG + " old size: " + oldSize);
Map<String, ?> map = sp.getAll();
Map<String, String> keyTypes = new HashMap<>();
ApplicationDelegate.log(TAG + " ---old data: ");
for (Map.Entry<String, ?> entry : map.entrySet()) {
Object obj = entry.getValue();
if (obj instanceof Boolean) {
keyTypes.put(entry.getKey(), "Boolean");
} else if (obj instanceof Integer) {
keyTypes.put(entry.getKey(), "Integer");
} else if (obj instanceof Long) {
keyTypes.put(entry.getKey(), "Long");
} else if (obj instanceof Float) {
keyTypes.put(entry.getKey(), "Float");
} else if (obj instanceof Double) {
keyTypes.put(entry.getKey(), "Double");
} else if (obj instanceof String) {
keyTypes.put(entry.getKey(), "String");
}
ApplicationDelegate.log(TAG + " key: " + entry.getKey() + " value: " + entry.getValue());
}
int imported = mmkv.importFromSharedPreferences(sp);
ApplicationDelegate.log(TAG + " imported size: " + imported);
if (BuildConfig.DEBUG) {
ApplicationDelegate.log(TAG + " ---imported data: ");
String[] keys = mmkv.allKeys();
for (int i = 0; i < keys.length; i++) {
String type = keyTypes.get(keys[i]);
if (type == null) {
continue;
}
switch (type) {
case "Boolean":
ApplicationDelegate.log(TAG + " key: " + keys[i] + " value: " + mmkv.decodeBool(keys[i]));
break;
case "Integer":
ApplicationDelegate.log(TAG + " key: " + keys[i] + " value: " + mmkv.decodeInt(keys[i]));
break;
case "Long":
ApplicationDelegate.log(TAG + " key: " + keys[i] + " value: " + mmkv.decodeLong(keys[i]));
break;
case "Float":
ApplicationDelegate.log(TAG + " key: " + keys[i] + " value: " + mmkv.decodeFloat(keys[i]));
break;
case "Double":
ApplicationDelegate.log(TAG + " key: " + keys[i] + " value: " + mmkv.decodeDouble(keys[i]));
break;
case "String":
ApplicationDelegate.log(TAG + " key: " + keys[i] + " value: " + mmkv.decodeString(keys[i]));
break;
default:
ApplicationDelegate.log(TAG + " key: " + keys[i] + " value: unknown type");
break;
}
}
}
if (imported > 0 && oldSize == imported) {
sp.edit().clear().apply();
}
if (oldSize != imported) {
ApplicationDelegate.log(TAG + " SharedPreferences 迁移失败, name=" + name);
ApplicationDelegate.bugTrace(ApplicationDelegate.MAIN_CODE_MMKV_IMPORT_FAIL, 0,
"name=" + name + ", old size=" + oldSize + ", import size=" + imported);
}
ApplicationDelegate.log(TAG + " ------" + name + "-----SharedPreferences迁移结束-----------");
}
}
这次替换和迁移目前已经上线验证 ,目前只发现在API 19的机器上出现崩溃,报java.lang.UnsatisfiedLinkError,MMKV的官网上有次问题的解答,可以参考解决。