Android疑難雜症之TimeoutException

1.分析緣由

在android開發中常常會到一些即便看了堆棧也沒法快速定位的問題,由於這些堆棧幾乎都是系統代碼,並沒有業務代碼,並且發生crash打印的堆棧也不必定是這個地方致使的。例如咱們今天要討論的java.util.concurrent.TimeoutException,咱們這裏能查詢到一個上報的堆棧以下:java

  • java.util.concurrent.TimeoutException: android.content.res.AssetManager.finalize() timed out after 10 seconds
  • android.content.res.AssetManager.destroy(Native Method)
  • android.content.res.AssetManager.finalize(AssetManager.java:591)
  • java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:250)
  • java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:237)
  • java.lang.Daemons$Daemon.run(Daemons.java:103)
  • java.lang.Thread.run(Thread.java:764)

能夠看到這些都是系統的堆棧,咱們也沒法快速定位到業務中究竟是哪裏致使了這個crash,只能從給出的堆棧知道是在系統回收資源AssetManager進行析構時超時致使的異常。android

上網查詢後發現,這其實已經算是一個比較廣泛的問題,並且大多發生在OPPO和360手機中,究其緣由:安全

  • Android在啓動後會建立一些守護線程,其中涉及到該問題的有兩個,分別是FinalizerDaemon和FinalizerWatchdogDaemon.函數

  • 對FinalizerDaemon析構守護線。對於重寫了成員函數finalize的對象,當它們被GC決定要被回收時,並不會立刻被回收,而是被放入到一個隊列中,等待FinalizerDaemon守護線程去調用它們的成員函數finalize後再被回收。this

  • FinalizerWatchdogDaemon析構監聽守護線程,用來監控FinalizerDaemon線程的執行。一旦監測到那些重寫了finalize的對象在執行成員函數finalize時超出必定時間,那麼就會退出VM。編碼

    從上面的分析知道,若是FinalizerDaemon進行對象析構時超過了MAX_FINALIZE_NANOS(默認10s,各個Rom廠商極可能會更改這個參數。例如OPPO不少機器上這個參數被改爲了120s),FinalizerWatchdogDaemon進行就會拋出TimeoutExceptionspa

    Daemons.java#FinalizerWatchdogDaemon線程

private static void finalizerTimedOut(Object object) {
    // The current object has exceeded the finalization deadline; abort!
    String message = object.getClass().getName() + ".finalize() timed out after "+ (MAX_FINALIZE_NANOS / NANOS_PER_SECOND) + " seconds";
    Exception syntheticException = new TimeoutException(message);
    ……
}
複製代碼

10s的超時實際上是很大的一個值,通常的析構方法的執行時間很難超過這個數。咱們大體推斷髮生這種crash的特色:3d

  • 從數據來看,崩潰都是應用處於後臺不可見的狀況下發生
  • 崩潰時應用已經被長時間使用

從Stack Overflow上找到了一個相對比較合理的出現場景:code

  • 當你的應用處於後臺,有對象須要釋放回收內存時
  • 記錄一個start_time,而後FinalizerDaemon開始析構AssetManager對象
  • 在這個過程當中,設備忽然進入了休眠狀態,析構執行被暫停
  • 當過了一段時間,設備被喚醒,析構任務被恢復,繼續執行,直至結束
  • 在析構完成後,獲得一個end_time
  • FinalizerWatchdogDaemon對end_time與start_time進行差值並與MAX_FINALIZE_NANOS比較,發現超過了MAX_FINALIZE_NANOS,因而就拋出了TimeOut異常

可見應用後臺執行的時間越長,出現的機率應該就會越大。

2.解決方案

咱們上面分析了發生這種TimeOut異常的緣由,知道要根治這個問題,仍是要合理的編碼,特別在涉及到內存分配方面時。那麼到底什麼纔是合理編碼,怎麼才能合理的申請的內存、複用內存和回收內存呢。這是一個仁者見仁智者見智的事情,也不是咱們本文討論的重點。這裏咱們提供一種折中的補救措施。就是在咱們的應用進程起來後,咱們經過反射主動關閉FinalizerWatchdogDaemon線程對析構過程的監聽,這樣即便FinalizerDaemon 調用對象的finalize進行析構回收超時了,也不會拋出這個TimeOut異常了。

private void stopWatchdogDaemon() {
    GLog.i(TAG, "---stopWatchdogDaemon---");
    try {
        /** * 1.獲取Daemons$FinalizerWatchdogDaemon的單例實例INSTANCE */
        Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
        Field field = clazz.getDeclaredField("INSTANCE");
        field.setAccessible(true);
        Object watchDog = field.get(null);
        try {
            /** * 2.將Daemon的成員變量thread設置爲null */
            Field thread = clazz.getSuperclass().getDeclaredField("thread");
            thread.setAccessible(true);
            thread.set(watchDog, null);
        } catch (Throwable throwable) {
            GLog.e(TAG, "set thread null to stop watchDog error, throwable: " + throwable.getMessage());
            try {
                /** * 3.若是2中將thread置null失敗,則直接調用Daemon的stop方法 */
                Method method = clazz.getSuperclass().getDeclaredMethod("stop");
                method.setAccessible(true);
                method.invoke(watchDog);
            } catch (Throwable error) {
                GLog.e(TAG, "invoke stop method to stop watchDog error, throwable: " + error.getMessage());
            }
        }
    } catch (Throwable throwable) {
        GLog.e(TAG, "get obj to stop watchDog error, throwable: " + throwable.getMessage());
    }
}
複製代碼

上面經過反射首先將FinalizerWatchdogDaemon父類Daemon中的thread置空,若是失敗再經過反射調用FinalizerWatchdogDaemon父類Daemon的stop方法繼續將成員變量thread置空(以下代碼中的1處註釋所示),並中止線程(下代碼中的2處註釋所示)

SDK=28 Daemons.java#Daemon

/** * Waits for the runtime thread to stop. This interrupts the thread * currently running the runnable and then waits for it to exit. */
public void stop() {
    Thread threadToStop;
    synchronized (this) {
        // 1.外部調用置空
        threadToStop = thread;
        thread = null;
    }
    if (threadToStop == null) {
        throw new IllegalStateException("not running");
    }
    // 2.中止線程
    interrupt(threadToStop);
    while (true) {
        try {
            threadToStop.join();
            return;
        } catch (InterruptedException ignored) {
        } catch (OutOfMemoryError ignored) {
            // An OOME may be thrown if allocating the InterruptedException failed.
        }
    }
}

// 3.線程安全的操做
public synchronized void interrupt(Thread thread) {
    if (thread == null) {
        throw new IllegalStateException("not running");
    }
    thread.interrupt();
}
複製代碼

Ps:在6.0以前,當使用stop方法來中止線程時,是一個不安全的操做,可能會存在線程安全問題。以下代碼所示

5.1.1
6.0

參考

  1. AssetManager.finalize() Timed Out After 10 Seconds分析
  2. 安卓開發中遇到的奇奇怪怪的問題(三)
相關文章
相關標籤/搜索