Android AssetManager finalize timed out 的坑

4,025 阅读2分钟

前言

最近遇到一个 Crash

java.util.concurrent.TimeoutException: android.content.res.AssetManager.finalize() timed out after 120 seconds
	at android.content.res.AssetManager.destroy(Native Method)
	at android.content.res.AssetManager.finalize(AssetManager.java:576)
	at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:217)
	at java.lang.Daemons$FinalizerDaemon.run(Daemons.java:200)
	at java.lang.Thread.run(Thread.java:818)

乍一看没有任何业务相关的代码,不过数量还挺多,而且大多数是 OPPO 手机。

原因

看 Crash 堆栈,猜测应该是资源回收超时了,不过具体原因还不清楚。从搜索引擎了解到,这个问题主要原因有以下原因:

  1. 对象 finalize() 方法耗时较长
    当 finalize() 方法中有耗时操作时,可能会出现方法执行超时。耗时操作一般有两种情况,一是方法内部确实有比较耗时的操作,比如 IO 操作,线程休眠等。
  2. 5.0 版本以下机型 GC 过程中 CPU 休眠导致
    有种观点认为系统可能会在执行 finalize() 方法时进入休眠, 然后被唤醒恢复运行后,会使用现在的时间戳和执行 finalize() 之前的时间戳计算耗时,如果休眠时间比较长,就会出现 TimeoutException。
  3. IO 负载过高
    许多类的 finalize() 都需要释放 IO 资源,当 APP 打开的文件数目过多,或者在多进程或多线程并发读取磁盘的情况下,随着并发数的增加,磁盘 IO 效率将大大下降,导致 finalize() 方法中的 IO 操作运行缓慢导致超时。
  4. FinalizerDaemon 中线程优先级过低
    FinalizerDaemon 中运行的线程是一个守护线程,该线程优先级一般为默认级别 (nice=0),其他高优先级线程获得了更多的 CPU 时间,在一些极端情况下高优先级线程抢占了大部分 CPU 时间,FinalizerDaemon 线程只能在 CPU 空闲时运行,这种情况也可能会导致超时情况的发生。(从 Android 8.0 版本开始,FinalizerDaemon 中守护线程优先级已经被提高,此类问题已经大幅减少)

解决方案

根本的解决方案是:优化代码,资源及时释放,从根源上避免资源回收超时。

不过由于代码量的问题,短期内无法尝试该方案,那么只有退而求其次,寻求缓解方案。

从网上看到关于该问题的解决方案主要有以下两个:

  1. 手动修改 finalize() 方法超时时间
  2. 手动停掉 FinalizerWatchdogDaemon 线程

直接说结论,这两种方法都是无效的,所以终极解决方案是

class ExceptionHandler : Thread.UncaughtExceptionHandler {
    private val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()

    override fun uncaughtException(t: Thread?, e: Throwable?) {
        if (t?.name == "FinalizerWatchdogDaemon" && e is TimeoutException) {
            // ignore it
        } else {
            defaultExceptionHandler.uncaughtException(t, e)
        }
    }
}

外部调用 Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler()) 替换异常处理器。

既然无法解决,那么就直接忽略它,避免对用户造成影响。

更新

评论区有大牛提出了更完美的解决方案,大家可以尝试一下。感谢 @wfwf

参考

江义旺:滴滴出行安卓端 finalize time out 的解决方案