Unity中出现的TimeoutException android.content.res.AssetManager$AssetInputStream in close缓解之策

本文介绍了Unity游戏引擎在Android平台遇到的TimeoutException崩溃问题,分析了崩溃原因,提出通过自定义异常处理器来缓解崩溃的方法,并详细阐述了测试过程及遇到的挑战,最终给出完善后的解决方案,强调了优化代码结构和良好编程习惯的重要性。


为什么标题说是“缓解之策”,因为这没法根治,如果要根治的话,请优化代码!

一、背景

最近项目组反馈,谷歌后台收到好多崩溃,如图
在这里插入图片描述

在这里插入图片描述
从堆栈中看,根本看不出是哪里导致的崩溃,只知道是TImeoutException!
哎,头大,怎么解决呢?!这么多崩溃,肯定得解决啊!

二、解决方案

1、分析问题

初步分析,导致该崩溃的原因为,AssetManager在释放资源的时候,调用AssetInputStream#close方法时,出现了超时,但比较坑的是,从堆栈信息看,并不知道是哪段代码出问题,先确保SDK的代码没有问题,找到SDK中使用到AssetManager的地方,用完务必要进行close操作!但是,问题并没有解决。还是继续报错!

2、寻找业界的解决方案

找到一篇讲得比较详细的文章:滴滴出行安卓端 finalize time out 的解决方案
这篇文章讲的主要思路,就是如何去避开这个崩溃,自定义Thread.UncaughtExceptionHandler,当捕捉到这个崩溃的时候,直接忽略,不让APP崩溃!

该崩溃出现的过程:

  1. FinalizerDaemon 执行对象 finalize() 超时。
  2. FinalizerWatchdogDaemon 检测到超时后,构造异常交给 Thread的defaultUncaughtExceptionHandler 调用 uncaughtException() 方法处理。
  3. APP 停止运行。

Thread 类的 defaultUncaughtExceptionHandler 我们很熟悉了,Java Crash 捕获一般都是通过设置 Thread.setDefaultUncaughtExceptionHandler() 方法设置一个自定义的 UncaughtExceptionHandler ,处理异常后通过链式调用,最后交给系统默认的 UncaughtExceptionHandler 去处理,在 Android 中默认的 UncaughtExceptionHandler 逻辑如下:

public class RuntimeInit {
   ...
  private static class UncaughtHandler implements Thread.UncaughtExceptionHandler {
      public void uncaughtException(Thread t, Throwable e) {
          try {
               ...
              // Bring up crash dialog, wait for it to be dismissed 展示APP停止运行对话框
              ActivityManagerNative.getDefault().handleApplicationCrash(
                      mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
          } catch (Throwable t2) {
               ...
          } finally {
              // Try everything to make sure this process goes away.
              Process.killProcess(Process.myPid()); //退出进程
              System.exit(10);
          }
      }
  }

   private static final void commonInit() {
       ...
       /* set default handler; this applies to all threads in the VM */
       Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
       ...
   }
}

从系统默认的 UncaughtExceptionHandler 中可以看出,APP Crash 时弹出的停止运行对话框以及退出进程操作都是在这里处理中处理的,那么只要不让这个代码继续执行就可以阻止 APP 停止运行了。基于这个思路可以将这个方案表示为如下的代码:

final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
   @Override
   public void uncaughtException(Thread t, Throwable e) {
       if (t.getName().equals("FinalizerWatchdogDaemon") && e instanceof TimeoutException) {
            //ignore it
       } else {
           defaultUncaughtExceptionHandler.uncaughtException(t, e);
       }
   }
});

可行性
这种方案在 FinalizerWatchdogDaemon 出现 TimeoutException 时主动忽略这个异常,阻断 UncaughtExceptionHandler 链式调用,使系统默认的 UncaughtExceptionHandler 不会被调用,APP 就不会停止运行而继续存活下去。由于这个过程用户无感知,对用户无明显影响,可以最大限度的减少对用户的影响。

优点

  1. 对系统侵入性小,不中断 FinalizerWatchdogDaemon 的运行。
  2. Thread.setDefaultUncaughtExceptionHandler() 方法是公开方法,兼容性比较好,可以适配目前所有 Android 版本。

三、测试及遇到的坑

1、编写代码并测试

按照上面的方案,在Demo编写测试用例,构造一个类,并在类的finalize中,使用Thread.sleep方法,模拟超时,即可触发TimeoutException

public class TimeoutTest {

    @Override
    protected void finalize() throws Throwable {
        LogUtils.e("TimeoutTest, start finalize");
        Thread.sleep(50_000);
        super.finalize();
        LogUtils.e("TimeoutTest, end finalize");
    }
}

在应用启动的时候,构造一个类,当系统回收这个对象的时候,就会触发finalize方法,则会抛异常

class MyApp: MultiDexApplication() {

    override fun onCreate() {
        super.onCreate()
        // 测试崩溃
        val test = TimeoutTest()
        // 拦截系统的崩溃
        val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler { t, e ->
            if (handleFinalizerWatchdogDaemonTimeoutException(t, e)){
                return@setDefaultUncaughtExceptionHandler
            }
            defaultHandler?.uncaughtException(t, e)
        }
    }
    
	/**
     * 处理AssetManager#AssetInputStream.finalize()时发生的TimeoutException问题
     */
    private fun handleFinalizerWatchdogDaemonTimeoutException(t: Thread, e: Throwable): Boolean{
        return if (t.name == "FinalizerWatchdogDaemon" && e is TimeoutException){
            LogUtils.e("ignore it.")
            true
        }else{
            false
        }
    }
}

进行测试,很ok,触发崩溃了,也被拦截了,没有崩溃!!!
在这里插入图片描述
这时候以为解决了,开心交差!并让项目组更新了SDK,期待线上的报错减少!

2、遇到的坑

然而!问题并没有解决!!这个崩溃数量并没有减少!!
好在加了埋点,发现被捕捉的异常很少!

(1)提出了疑问

  1. 设置的异常处理器被忽略了?
  2. 设置的过滤TimeoutException的条件不对?
  3. Unity做了什么处理?

(2)带着这些疑问,一步一步尝试

  1. 设置的异常处理器被忽略了?
    没有被忽略,一般的SDK,处理完自身的异常后,都会往下抛,让别的异常处理器处理
  2. 设置的过滤TimeoutException的条件不对?
    通过调试,发现确实条件不对,再次发现,对应的Throwable不是TimeoutException,为什么呢?
  3. Unity做了什么处理?
    找到Unity处理异常的时候,对Throwable进行了一层包装!所以问题2的条件不对!
public final synchronized void uncaughtException(Thread var1, Throwable var2) {
        try {
            Error var3;
            (var3 = new Error(String.format("FATAL EXCEPTION [%s]\n", var1.getName()) + String.format("Unity version     : %s\n", "2019.3.13f1") + String.format("Device model      : %s %s\n", Build.MANUFACTURER, Build.MODEL) + String.format("Device fingerprint: %s\n", Build.FINGERPRINT))).setStackTrace(new StackTraceElement[0]);
            var3.initCause(var2);
            this.a.uncaughtException(var1, var3);
        } catch (Throwable var4) {
            this.a.uncaughtException(var1, var2);
        }
    }

3、最终的解决方案

完善判断是否为TimeoutException的条件

    /**
     * 避免被别的UncaughtExceptionHandler包装了Throwable
     * 目前知道的是Unity会包装一层
     */
    private fun isTimeoutException(e: Throwable): Boolean{
        var cause: Throwable? = e
        while (cause != null){
            if (cause is TimeoutException){
                return true
            }
            cause = e.cause
        }
        return false
    }

最终的代码如下

class MyApp: MultiDexApplication() {

    override fun onCreate() {
        super.onCreate()       
        // 测试崩溃
        val test = TimeoutTest()
        // 拦截系统的崩溃
        val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler { t, e ->
            if (handleFinalizerWatchdogDaemonTimeoutException(t, e)){
                return@setDefaultUncaughtExceptionHandler
            }
            defaultHandler?.uncaughtException(t, e)
        }
    }

    /**
     * 处理AssetManager#AssetInputStream.finalize()时发生的TimeoutException问题
     */
    private fun handleFinalizerWatchdogDaemonTimeoutException(t: Thread, e: Throwable): Boolean{
        return if (t.name == "FinalizerWatchdogDaemon" && e is TimeoutException){
            LogUtils.e("ignore it.")
            true
        }else{
            false
        }
    }

    /**
     * 避免被别的UncaughtExceptionHandler包装了Throwable
     * 目前知道的是Unity会包装一层
     */
    private fun isTimeoutException(e: Throwable): Boolean{
        var cause: Throwable? = e
        while (cause != null){
            if (cause is TimeoutException){
                return true
            }
            cause = e.cause
        }
        return false
    }
}

四、总结

这样可以缓解崩溃,但是治标不治本,没有从根源上解决。对于这类问题来说,虽然人为阻止了 Crash,避免了 APP 停止,APP 能够继续运行,但是 finalize() 超时还是客观存在的,如果 finalize() 一直超时的状况得不到缓解,将会导致 FinalizerDaemon 中 FinalizerReference 队列不断增长,最终出现 OOM 。因此还需要从一点一滴做起,优化代码结构,培养良好的代码习惯,从而彻底解决这个问题。当然 BUG 不断,优化不止,在解决问题的路上,缓解止损措施也是非常重要的手段。谁能说能抓老鼠的白猫不是好猫呢?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值