庖丁解牛之SharedPreferences超級(jí)大卡頓

背景

?最近在排查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ì)卡住主線程

參考資料

http://blog.chinaunix.net/uid-29506893-id-5761774.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容