背景
?最近在排查app卡頓問(wèn)題,在公司內(nèi)部的bug管理平臺(tái)上發(fā)現(xiàn)這個(gè)類(lèi)卡頓問(wèn)題,知道卡頓了多長(zhǎng)時(shí)間嗎,足足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)
? 剛開(kāi)始以為是系統(tǒng)Unsafe的卡頓,就沒(méi)怎么細(xì)看,后來(lái)發(fā)現(xiàn)不對(duì)中間居然看見(jiàn)了SharedPreferences的代碼,之前就知道SharedPreferences這個(gè)玩意坑很多,我又回憶起之前面試一個(gè)面試者,他提到過(guò)如何采用objectbox替換SharedPreferences解決卡頓問(wèn)題,我懷疑就是這玩意導(dǎo)致的,這激起了我的好奇心
問(wèn)題原因分析
? 項(xiàng)目中用了SharedPreferences 這個(gè)玩意,誰(shuí)知道這額玩意有大坑呀,給我們app卡的不行不行的,代碼在apply的時(shí)候,SharedPreferences 內(nèi)部發(fā)送了一個(gè)異步任務(wù)取執(zhí)行文件的寫(xiě)操作,按道理說(shuō)寫(xiě)操作都是在異步線程中執(zhí)行的,不應(yīng)該會(huì)卡頓主線程呀,是的,讀寫(xiě)操作時(shí)在異步線程,QueuedWork.waitToFinish 這個(gè)方法是在主線程中執(zhí)行,具體的調(diào)用到代碼在ActiviytThread 類(lèi)的handleStopActivity 方法和handleServiceArgs 方法中等多處方法中有調(diào)用,我們出問(wèn)題的地方就是調(diào)用了handleServiceArgs方法,QueuedWork.waitToFinish 這個(gè)方法中執(zhí)行了線程操作,所以導(dǎo)致了主線程卡住了
commit 造成的卡頓
?我們先看一下SharedPreferencesImpl 這個(gè)類(lèi),這個(gè)類(lèi)是具體的實(shí)現(xiàn)類(lèi),我們看一下commit方法
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory();
//這地方是執(zhí)行具體的寫(xiě)入任務(wù)
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//看看沒(méi)這個(gè)地方就讓主線程卡住的原因
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方法中,首先執(zhí)行寫(xiě)入任務(wù)也就是enqueueDiskWrite這個(gè)方法,我們稍后分析,然后讓調(diào)用線程處于等待狀態(tài),當(dāng)寫(xiě)入任務(wù)執(zhí)行成功后喚起調(diào)用commit的線程,假設(shè)調(diào)用commit的線程就是主線線程,并且寫(xiě)入任務(wù)耗時(shí)還比較多的,這不就阻塞住主線程了嗎?
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;
}
}
//往系統(tǒng)的隊(duì)列中發(fā)送任務(wù),然后在工作線程中執(zhí)行任務(wù)
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
?enqueueDiskWrite 方法中首先判斷的postWriteRunnable 是否等于null,如果等于空了,就在當(dāng)前調(diào)用的地方執(zhí)行寫(xiě)入操作,如果不是就往QueuedWork 隊(duì)列中發(fā)送任務(wù)
總結(jié)一下:如果是使用commit方式提交,會(huì)阻塞調(diào)用commit方法的線程,如果寫(xiě)入任務(wù)很多比較耗時(shí),就卡住了,所以不要在主線程執(zhí)行寫(xiě)入文件的操作,但是我們上線卡頓日志是另外一種情況,是使用了apply提交的時(shí)候才會(huì)出現(xiàn)的
apply造成的卡頓
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//這個(gè)地方是造成卡頓的原因
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是執(zhí)行異步任務(wù)的方法,我們之前已經(jīng)見(jiàn)過(guò)這個(gè)方法,在apply方法中調(diào)用enqueueDiskWrite方法的時(shí)候最后一個(gè)參數(shù)是不等于空的,也就是說(shuō)我們要執(zhí)行一個(gè)異步任務(wù),最終這異步任務(wù)的執(zhí)行是在QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)方法中
QueuedWork是干什么的呢?
?QueuedWork就是android系統(tǒng)提供的一個(gè)執(zhí)行異步任務(wù)的工具類(lèi),內(nèi)部的實(shí)現(xiàn)邏輯的就是創(chuàng)建一個(gè)HandlerThread作為工作線程,然后QueuedWorkHandler和這個(gè)HandlerThread進(jìn)行管理,每當(dāng)有任務(wù)添加進(jìn)來(lái)就在這個(gè)異步線程中執(zhí)行,這個(gè)異步線程的名字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 添加一個(gè)任務(wù),sWork是一個(gè)LinkedList,這個(gè)隊(duì)列中數(shù)據(jù)最終在queued-work-looper 線程中依次得到執(zhí)行
創(chuàng)建handle的過(guò)程
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<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) 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");
}
}
}
}
實(shí)際上就是遍歷sWork,挨個(gè)執(zhí)行任務(wù),
那為什么會(huì)出現(xiàn)上面的卡頓了?
?apply的中寫(xiě)入操作也是在異步線程執(zhí)行,不會(huì)導(dǎo)致主線程卡頓,但是如果異步任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng),當(dāng)ActvityThread執(zhí)行了handleStopActivity或者h(yuǎn)andleServiceArgs或者h(yuǎn)andlePauseActivity 等方法的時(shí)候都會(huì)調(diào)用QueuedWork.waitToFinish()方法,而此方法中會(huì)在異步任務(wù)執(zhí)行完成前一直阻塞住主線程,所以卡頓問(wèn)題就產(chǎn)生了
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) {
//關(guān)鍵代碼
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: ");
}
}
}
}
?會(huì)從sFinishers隊(duì)列中取出數(shù)據(jù)然后執(zhí)行run方法,我們別忘了在apply的方法中,我們還添加了QueuedWork.addFinisher(awaitCommit);這個(gè)awaitCommit 就得到執(zhí)行了但是awaitCommit中的代碼確實(shí)是阻塞的代碼,等待寫(xiě)入線程執(zhí)行完畢才能喚起此線程
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中的寫(xiě)入代碼不執(zhí)行完,主線程就一直卡住了,也就出現(xiàn)了我們上面的問(wèn)題
SharedPreferences 獲取數(shù)據(jù)也是阻塞的
我們看一下SharedPreferences 的初始化代碼
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
數(shù)據(jù)加載是異步線程執(zhí)行的
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
那為什么獲取數(shù)據(jù)也是阻塞的?
看一下獲取數(shù)據(jù)的get方法
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
關(guān)鍵awaitLoadedLocked 這個(gè)方法,當(dāng)數(shù)據(jù)沒(méi)有加載完,就讓調(diào)用的線程處于等待中,阻塞住了
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) {
}
}
}
總結(jié)
- commit 方式會(huì)阻塞調(diào)用的線程
- apply 放法不會(huì)阻塞調(diào)用的線程,但是如果寫(xiě)入任務(wù)比較耗時(shí),會(huì)阻塞住主線程,因?yàn)橹骶€程有調(diào)用的代碼,需要等寫(xiě)入任務(wù)執(zhí)行完了才會(huì)繼續(xù)往下執(zhí)行
建議
?SharePrefereces這玩意能不用就不用,坑還是比較隱晦的,如果不查看源代碼是根本不可能知道的,一般大家用這個(gè)類(lèi)主要是圖簡(jiǎn)單省事 了,我就是存一個(gè)數(shù)據(jù)搞那么復(fù)雜干什么呢?我建議如果大家在線上的項(xiàng)目中存儲(chǔ)數(shù)據(jù)還是用自己實(shí)現(xiàn)的方案吧,不要用這個(gè)了,如果非要用這個(gè)我建議開(kāi)啟一個(gè)線程然后在線程中調(diào)用commit方式更新數(shù)據(jù),至少這個(gè)方案不會(huì)卡住主線程