反射解決安卓里的TimeoutException

TimeoutException

錯誤堆棧信息:

FinalizerWatchdogDaemon
java.util.concurrent.TimeoutException
android.os.BinderProxy.finalize() timed out after 120 seconds
android.os.BinderProxy.destroy(Native Method)
android.os.BinderProxy.finalize(Binder.java:547)
java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:214)
java.lang.Daemons$FinalizerDaemon.run(Daemons.java:193)
java.lang.Thread.run(Thread.java:818)

首先來說明一下發(fā)生問題的原因,在GC時,為了減少應(yīng)用程序的停頓,會啟動四個GC相關(guān)的守護(hù)線程。FinalizerWatchdogDaemon就是其中之一,它是用來監(jiān)控FinalizerDaemon線程的執(zhí)行。

FinalizerDaemon:析構(gòu)守護(hù)線程。對于重寫了成員函數(shù)finalize的對象,它們被GC決定回收時,并沒有馬上被回收,而是被放入到一個隊列中,等待FinalizerDaemon守護(hù)線程去調(diào)用它們的成員函數(shù)finalize,然后再被回收。

一旦檢測到執(zhí)行成員函數(shù)finalize時超出一定的時間,那么就會退出VM。我們可以理解為GC超時了。這個時間默認(rèn)為10s,我通過翻看oppo、華為的Framework源碼發(fā)現(xiàn)這個時間在部分機(jī)型被改為了120s和30s。

image.png

雖然時間加長了,但還是一樣的超時了,具體在oppo手機(jī)上為何這么慢,暫時無法得知,但是可以肯定的是Finalizer對象過多導(dǎo)致的。知道了原因,所以要模擬這個問題也很簡單了。也就是引用一個重寫finalize方法的實例,同時這個finalize方法有耗時操作,這時我們手動GC就行了。剛好前幾天,在我訂閱的張紹文老師的《Android開發(fā)高手課中》,老師提到了這個問題,同時分享了一個模擬問題并解決問題的 Demo。有興趣的可以試試。

那么解決問題的方法也就來了,我們可以在ApplicationattachBaseContext中調(diào)用(可以針對問題機(jī)型及系統(tǒng)版本去處理,不要矯枉過正):

try {
            final Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
            final Field field = clazz.getDeclaredField("INSTANCE");
            field.setAccessible(true);
            final Object watchdog = field.get(null);
            try {
                final Field thread = clazz.getSuperclass().getDeclaredField("thread");
                thread.setAccessible(true);
                thread.set(watchdog, null);
            } catch (final Throwable t) {
                Log.e(TAG, "stopWatchDog, set null occur error:" + t);

                t.printStackTrace();
                try {
                    // 直接調(diào)用stop方法,在Android 6.0之前會有線程安全問題
                    final Method method = clazz.getSuperclass().getDeclaredMethod("stop");
                    method.setAccessible(true);
                    method.invoke(watchdog);
                } catch (final Throwable e) {
                    Log.e(TAG, "stopWatchDog, stop occur error:" + t);
                    t.printStackTrace();
                }
            }
        } catch (final Throwable t) {
            Log.e(TAG, "stopWatchDog, get object occur error:" + t);
            t.printStackTrace();
        }

其實我是用的是stackoverflow這篇帖子中提供的方法:

public static void fix() {
    try {
        Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");

        Method method = clazz.getSuperclass().getDeclaredMethod("stop");
        method.setAccessible(true);

        Field field = clazz.getDeclaredField("INSTANCE");
        field.setAccessible(true);

        method.invoke(field.get(null));

    }
    catch (Throwable e) {
        e.printStackTrace();
    }
}

兩種方法都是通過反射最終將FinalizerWatchdogDaemon中的thread置空,這樣也就不會執(zhí)行此線程,所以不會再有超時異常發(fā)生。推薦老師的方法,更加全面完善。因為在Android 6.0之前會有線程安全問題,如果直接調(diào)用stop方法,還是會有幾率觸發(fā)此異常。5.0源代碼如下:

private static abstract class Daemon implements Runnable {

        private Thread thread;// 一種是直接置空thread

        public synchronized void start() {
            if (thread != null) {
                throw new IllegalStateException("already running");
            }
            thread = new Thread(ThreadGroup.systemThreadGroup, this, getClass().getSimpleName());
            thread.setDaemon(true);
            thread.start();
        }

        public abstract void run();

        protected synchronized boolean isRunning() {
            return thread != null;
        }

        public synchronized void interrupt() {
            if (thread == null) {
                throw new IllegalStateException("not running");
            }
            thread.interrupt();
        }

        public void stop() {
            Thread threadToStop;
            synchronized (this) {
                threadToStop = thread;
                thread = null; // 一種是通過調(diào)用stop置空thread
            }
            if (threadToStop == null) {
                throw new IllegalStateException("not running");
            }
            threadToStop.interrupt();
            while (true) {
                try {
                    threadToStop.join();
                    return;
                } catch (InterruptedException ignored) {
                }
            }
        }

        public synchronized StackTraceElement[] getStackTrace() {
            return thread != null ? thread.getStackTrace() : EmptyArray.STACK_TRACE_ELEMENT;
        }
    }

這個所謂的線程安全問題就在stop方法中的threadToStop.interrupt()。在6.0開始,這里變?yōu)榱?code>interrupt(threadToStop),而interrupt方法加了同步鎖。

public synchronized void interrupt(Thread thread) {
     if (thread == null) {
         throw new IllegalStateException("not running");
     }
     thread.interrupt();       
}

雖然崩潰不會出現(xiàn)了,但是問題依然存在,可謂治標(biāo)不治本。通過這個問題也提醒我們,盡量避免重寫finalize方法,同時不要在其中有耗時操作。其實我們Android中的View都有實現(xiàn)finalize方法,那么減少View的創(chuàng)建就是一種解決方法。

強(qiáng)烈推薦閱讀提升Android下內(nèi)存的使用意識和排查能力、再談Finalizer對象–大型App中內(nèi)存與性能的隱性殺手

3.SchedulerPoolFactory

前一陣在用Android Studio的內(nèi)存分析工具檢測App時,發(fā)現(xiàn)每隔一秒,都會新分配出20多個實例,跟蹤了一下發(fā)現(xiàn)是RxJava2中的SchedulerPoolFactory創(chuàng)建的。

[圖片上傳失敗...(image-f684ec-1553680065566)]

一般來說如果一個頁面創(chuàng)建加載好后是不會再有新的內(nèi)存分配,除非頁面有動畫、輪播圖、EditText的光標(biāo)閃動等頁面變化。當(dāng)然了在應(yīng)用退到后臺時,或者頁面不可見時,我們會停止這些任務(wù)。保證不做這些無用的操作。然而我在后臺時,這個線程池還在不斷運(yùn)行著,也就是說CPU在周期性負(fù)載,自然也會耗電。那么就要想辦法優(yōu)化一下了。

SchedulerPoolFactory 的作用是管理 ScheduledExecutorServices的創(chuàng)建并清除。

SchedulerPoolFactory 部分源碼如下:

static void tryStart(boolean purgeEnabled) {
        if (purgeEnabled) {
            for (;;) { // 一個死循環(huán)
                ScheduledExecutorService curr = PURGE_THREAD.get();
                if (curr != null) {
                    return;
                }
                ScheduledExecutorService next = Executors.newScheduledThreadPool(1, new RxThreadFactory("RxSchedulerPurge"));
                if (PURGE_THREAD.compareAndSet(curr, next)) {

            // RxSchedulerPurge線程池,每隔1s清除一次
                    next.scheduleAtFixedRate(new ScheduledTask(), PURGE_PERIOD_SECONDS, PURGE_PERIOD_SECONDS, TimeUnit.SECONDS);

                    return;
                } else {
                    next.shutdownNow();
                }
            }
        }
    }

   static final class ScheduledTask implements Runnable {
        @Override
        public void run() {
            for (ScheduledThreadPoolExecutor e : new ArrayList<ScheduledThreadPoolExecutor>(POOLS.keySet())) {
                if (e.isShutdown()) {
                    POOLS.remove(e); 
                } else {
                    e.purge();//圖中154行,purge方法可用于移除那些已被取消的Future。
                }
            }
        }
    }

我查了相關(guān)問題,在stackoverflow找到了此問題,同時也給RxJava提了Issue,得到了回復(fù)是可以使用:

// 修改周期時間為一小時
 System.setProperty("rx2.purge-period-seconds", "3600");

當(dāng)然你也可以關(guān)閉周期清除:

System.setProperty("rx2.purge-enabled", false);

作用范圍如下:

static final class PurgeProperties {

        boolean purgeEnable;

        int purgePeriod;

        void load(Properties properties) {
            if (properties.containsKey(PURGE_ENABLED_KEY)) {
                purgeEnable = Boolean.parseBoolean(properties.getProperty(PURGE_ENABLED_KEY));
            } else {
                purgeEnable = true; // 默認(rèn)是true
            }

            if (purgeEnable && properties.containsKey(PURGE_PERIOD_SECONDS_KEY)) {
                try {
                    // 可以修改周期時間
                    purgePeriod = Integer.parseInt(properties.getProperty(PURGE_PERIOD_SECONDS_KEY));
                } catch (NumberFormatException ex) {
                    purgePeriod = 1; // 默認(rèn)是1s
                }
            } else {
                purgePeriod = 1; // 默認(rèn)是1s
            }
        }
    }

1s的清除周期我覺得有點(diǎn)太頻繁了,最終我決定將周期時長改為60s。最好在首次使用RxJava前修改,放到Application中最好。

4.其他

  • 適配8.0時注意Service的創(chuàng)建。否則會有IllegalStateException異常:
java.lang.IllegalStateException:Not allowed to start service Intent { xxx.MyService }: app is in background uid null
  • 有些手機(jī)(已知oppo)在手機(jī)儲存空間不足時,當(dāng)你應(yīng)用退到后臺時會自動清除cache下文件,所以如果你有重要數(shù)據(jù)存儲,避免放在cache下,否則當(dāng)你再次進(jìn)入應(yīng)用時,再次獲取數(shù)據(jù)時會有空指針。例如有使用磁盤緩存 DiskLruCache 來存儲數(shù)據(jù)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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