简述
SharedPreference(以下简称 sp)是Android系统一套轻量级的数据持久化方案,因为使用简单而受到开发者的喜爱。在项目实际开发中,因开发者滥用sp导致ANR问题。接下来分析一下产生ANR问题的原因,以及解决方案。
ANR产生的原因
1.SP第一次加载数据需要全量加载,当数据量大时可能会阻塞UI线程造成卡顿: sp文件创建后,会创建一个线程会加载解析对应的sp文件。但是当ui线程要访问sp中的内容时,如果此时sp文件还未完全加载解析到内存,此时UI线程会被block,直接sp文件加载到内存中为止。
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) {
// sp未加载到内存时,不可用
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// 省略。。。。
}
sp在读或写时,都会走到 awaitLoadedLocke()逻辑 ,在mLock =false即sp未加载到内存,此时读写会block在 mLock锁上,直到loadFromDisk()执行完毕。
@GuardedBy("mLock")
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);
}
}
@Override
public Map<String, ?> getAll() {
synchronized (mLock) {
awaitLoadedLocked();
//noinspection unchecked
return new HashMap<String, Object>(mMap);
}
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
2.commit/apply操作可能造成ANR commit 是同步操作,会在主线程直接操作io,当写入操作比较耗时就会导致ui线程阻塞,进而产生ANR;apply虽然是异步提交,但是异步写入磁盘时,如果执行了Activity/Service/BroadcastRecieve中的Stop方法,那么一样会等待sp写入完毕,等待时间过长也会引起ANR。针对apply()展开看一下:
@Override
public void apply() {
// 同步写入到内存
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 异步写入到磁盘
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
构造一个名为awaitCommit的Runnable任务并将其加入到QueuedWork中,该任务内部直接调用了CountDownLatch.await()方法,即直接在UI线程执行等待操作,那么需要看QueuedWork中何时执行这个任务。
public static void waitToFinish() {
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
// 从队列中取出任务
finisher = sFinishers.poll();
}
// 如果任务为空,则跳出循环,UI线程可以继续往下执行
if (finisher == null) {
break;
}
// 任务不为空,执行 CountDownLatch.await(),即UI线程会阻塞等待
finisher.run();
}
} finally {
sCanDelay = true;
}
ActivityThread
private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
//......其他......
QueuedWork.waitToFinish();
}
private void handleStopService(IBinder token) {
//......其他......
QueuedWork.waitToFinish();
}
省略了一些代码细节,可以看到在ActivityThread中handleStopActivity、handleStopService方法中都会调用waitToFinish()方法,即在Activity的onStop()中、Service的onStop()中都会先同步等待写入任务完成才会继续执行。
所以apply()虽然是异步写入磁盘,但是如果此时执行到Activity/Service的onStop(),依然可能会阻塞UI线程导致ANR。
解决方案
- 今日头条方案mp.weixin.qq.com/s/kfF83UmsG…
- 对sp进行迁移(MMKV或DataStore)
- 不在sp中存储非轻量级的数据
- 限制每个sp文件大小,将不相关的配置项保存在不同的文件中
- 预加载sp对象
其他
commit和apply异同:commit 提交后有结果返回,apply没有返回值。都会把数据同步提交到内存中,apply再异常提交到磁盘,commit是同步提交到磁盘。