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。

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