震惊!java竟然catch不到异常?

Bug引入分析

最近的问题上报平台上上报了一个OOM问题,在Activity中setContentView时候子View因为decode了较大的Drawable导致了OOM。该OOM问题是一个常规的OOM问题,但是在修改这个bug的时候发现了一个神奇的现象,外围Activity的setContentView中已经添加了Try Catch函数,但是并没有Catch住OOM异常。

外围Try Catch分析

查看相关的代码,在Activity的初始化的时候确实已经进行了异常的捕获,看来当前Activity的OOM问题也是由来已久,虽然已经进行了图片的统一管理,也及时的释放相关的图片资源,但是在不同机型上的OOM问题还是时有发生,所以这里对OOM异常进行了捕获,如果捕获了相关的异常,那就释放相关的内存,尝试再次的加载当前页面。但是实际上这儿并没有catch到相关的异常。那究竟是为什么没有Catch到异常?外围已经进行了Try Catch,为什么没有Catch到呢?

Try Catch代码如下所示:

        try {
            setContentView(R.layout.xxxxxx);
            init();
        } catch (OutOfMemoryError ex) {
            // 先清理下图片缓存,再尝试一次
            BaseApplicationImpl.sImageCache.clear();
            try {
                setContentView(R.layout.xxxxx);
                init();
            } catch (Throwable exx) {
                finish();
            }
       }

通过模拟复现场景

由于线上OOM的异常难以复现,所以本次分析直接在本地进行一次OOM的模拟复现,通过在XML文件中加载一个OOM异常的View

查看相关的日志,发现控制台上会打印多个异常,一个是RuntimeException,一个是InflateException,一个是InvocationTargetException,最后才是我们的OutOfMemoryError,竟然会同时打印了多个,这里不是只有一个OOM异常吗?
日志打印如下:

(为什么会同时打印下面的异常,不是是一个OOM的问题吗)
**********************
Process: com.example.myapplication, PID: 28056
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.myapplication/com.example.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #9: Binary XML file line #9: Error inflating class com.example.myapplication.MyView
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3318)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3429)
**********************

(打印了InflateException)
**********************
Caused by: android.view.InflateException: Binary XML file line #9: Binary XML file line #9: Error inflating class com.example.myapplication.MyView

Caused by: android.view.InflateException: Binary XML file line #9: Error inflating class com.example.myapplication.MyView
**********************

(打印了InvocationTargetException)
**********************
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Constructor.newInstance0(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:334)
at android.view.LayoutInflater.createView(LayoutInflater.java:658)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:801)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:741)
**********************

(只有OutOfMemoryError会清晰的打印当前的问题详情)
**********************
Caused by: java.lang.OutOfMemoryError: Failed to allocate a 124606360 byte allocation with 25165824 free bytes and 111MB until OOM, max allowed footprint 109536344, growth limit 201326592
at java.util.Arrays.copyOf(Arrays.java:3139)
at java.util.Arrays.copyOf(Arrays.java:3109)
at java.util.ArrayList.grow(ArrayList.java:275)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:249)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:241)
at java.util.ArrayList.add(ArrayList.java:467)
at com.example.myapplication.MyView.init(MyView.java:29)
at com.example.myapplication.MyView.<init>(MyView.java:17)
at java.lang.reflect.Constructor.newInstance0(Native Method) 
at java.lang.reflect.Constructor.newInstance(Constructor.java:334) 
at android.view.LayoutInflater.createView(LayoutInflater.java:658) 
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:801) 
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:741) 
at android.view.LayoutInflater.rInflate(LayoutInflater.java:874) 
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:835) 
at android.view.LayoutInflater.inflate(LayoutInflater.java:515) 
at android.view.LayoutInflater.inflate(LayoutInflater.java:423) 
at android.view.LayoutInflater.inflate(LayoutInflater.java:374) 
at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:469) 
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:140) 
at com.example.myapplication.MainActivity.onCreate(MainActivity.java:14) 
**********************

观察几个异常的区别,发现只有OutOfMemoryError会准确的打印出当前的异常位置,InvocationTargetException仅仅只会打印到newInstance0的位置,InflateException就更加简单了,仅仅提出了当前的问题,这一连串的异常到底是怎么联系起来的?并且我的OutOfMemoryError为什么没有被捕获?

分析InflateException

分析问题,首先要熟悉问题发生的流程,为了贴近这次问题,我们通过异常栈对整个XML的加载流程进行分析,整个XML的加载流程如下所示,为了更加直观,本图例省略了部分流程。
在这里插入图片描述
可以看到整个xml加载的流程为MainActivity.setContentView-----> 通过LayoutInflater开始解析XML ----->进行子View的解析---->进行子View的具体解析并递归创建View---->通过Tag创建View----->通过Constructor反射加载View---->调用newInstance0 native方法

分析问题肯定是从源头抓起,通过流程的分析,可以看到在创建出对象前,最后实际调用的是LayoutInflater.createView()方法,马上打开createView方法一探究竟。

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
            
        //初始化Constructor
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;
        
        try {
           //这里通过constructor.newInstance(args)反射创建了一个View
           ***************
           final View view = constructor.newInstance(args);
           ****************
         
         //对抛出的异常进行包装,均包装为InflateException
        } catch (NoSuchMethodException e) {
            final InflateException ie = new InflateException(***;
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (ClassCastException e) {
            // If loaded class is not a View subclass
            final InflateException ie = new InflateException(***;
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
            
        } catch (ClassNotFoundException e) {
            // If loadClass fails, we should propagate the exception.
            throw e;
            
        } catch (Exception e) {
            final InflateException ie = new InflateException(***);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

可以看到createView方法会捕获从newInstance中抛出的异常,同时将异常通过InflateException进行了包装,咦,出现了InflateException,好像和上面的异常联系起来了?但是仔细一想好像又并没有什么关系,因为我们代码中实际抛出的是OutOfMemoryError,这里一系列Exception的Catch好像实际不能捕获到我们的异常。
在这里插入图片描述

分析InvocationTargetException

分析了InflateException,心中不免有个疑问,既然OutOfMemoryError不能被Exception捕获,那这里应该会有Exception的异常抛出,那会不会是在下面的方法中抛出了Exception类型的异常?继续向下分析,果然,在constructor.newInstance方法中可能会抛出了一个我们非常熟悉的异常InvocationTargetException,这不就是上面异常栈中出现过的异常吗。

constructor.newInstance方法源码如下:

 public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
       {
        if (serializationClass == null) {
            return newInstance0(initargs);
        } else {
            return (T) newInstanceFromSerialization(serializationCtor, serializationClass);
        }
    }

从代码中可以看到,对于serializationClass为null的class,会通过newInstance0进行反射加载Class。newInstance0又是做了啥?继续翻看代码,在源码中的newInstance0代码如下:

private native T newInstance0(Object... args) throws InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException;

一看newInstance0方法,是个native调用的方法?native方法在Android studio中并不能直观的看到,马上开始下载Android的源码,进入native的源码分析。

下载完毕,通过全局搜索newInstance0,发现newInstance0实际是在java_lang_reflect_Constructor.cc中进行实现的,源码如下:

static jobject Constructor_newInstance0(JNIEnv* env, jobject javaMethod, jobjectArray javaArgs) {
  ScopedFastNativeObjectAccess soa(env);
  ObjPtr<mirror::Constructor> m = soa.Decode<mirror::Constructor>(javaMethod);
  StackHandleScope<1> hs(soa.Self());
  Handle<mirror::Class> c(hs.NewHandle(m->GetDeclaringClass()));
  
  //省略对于Abstract类的操作
  //省略对于不是Accessible class以及不是public class的操作
  //省略对于当前类没有Initialized的操作
  //省略对于StringClass的操作
  ******************
  //执行构造方法
  InvokeMethod(soa, javaMethod, javaReceiver, javaArgs, 2);
  return javaReceiver;
}

为了更加直观,这里省略了大部分的代码,由于本篇讨论的问题只涉及执行构造函数时候出现的问题,所以主要关注的地方仅仅放在最后的InvokeMethod即可,有兴趣的同学可以去了解下其他的具体实现。

同样的方法可以查找到InvokeMethod是位于reflect.cc中的函数,InvokeMethod方法的源码如下:

jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
                     jobject javaReceiver, jobject javaArgs, size_t num_frames) {
  
  //省略对于堆栈溢出的处理
  //省略当前类已经Initialized的判断
  //省略当前类如果不是static的操作
  //省略当前类是否是accessible
  //如果当前栈已经溢出
  **************************
  
  //实际通过参数执行相关构造方法的函数
  InvokeWithArgArray(soa, m, &arg_array, &result, shorty);

  //本篇讨论的重点,关于异常的处理
   if (soa.Self()->IsExceptionPending()) {
    //首先判断是否有异常产生,如果有异常,则清除掉异常
    jthrowable th = soa.Env()->ExceptionOccurred();
    soa.Self()->ClearException();
    
    //初始化InvocationTargetException
    jclass exception_class = soa.Env()->FindClass("java/lang/reflect/InvocationTargetException");
    if (exception_class == nullptr) {
      soa.Self()->AssertPendingException();
      return nullptr;
    }
    
   //拿到产生异常的信息
    jmethodID mid = soa.Env()->GetMethodID(exception_class, "<init>", "(Ljava/lang/Throwable;)V");
    CHECK(mid != nullptr);

   //创建一个异常
    jobject exception_instance = soa.Env()->NewObject(exception_class, mid, th);
    if (exception_instance == nullptr) {
      soa.Self()->AssertPendingException();
      return nullptr;
    }
    //将当前异常通过InvocationTargetException包装
    soa.Env()->Throw(reinterpret_cast<jthrowable>(exception_instance));
    return nullptr;
 }

  return soa.AddLocalReference<jobject>(BoxPrimitive(Primitive::GetType(shorty[0]), result));
}

咦原来是这里对Exception进行了处理,通过源码可以看到总体流程就是判断方法的执行是否产生了异常,如果产生了,则清除异常,同时初始化InvocationTargetException,并将拿到异常的信息,通过InvocationTargetException进行包装,再抛出当前的异常。

分析到这里,问题基本也明晰了,因为在invokeMethod方法中,对程序运行过程中产生的
OutOfMemoryError进行了包装,这样在外围抛出的异常就变成了InvocationTargetException,InvocationTargetException再经过creatView方法的捕获,转换为了InflateException,所以导致我们直接捕获OutOfMemoryError失败。

但是可能大家会问了,既然这里已经clear了Exception,同时已经进行了包装,那为什么还是打印了OutOfMemoryError?对啊,为什么呢。继续查看native源码。

分析OutOfMemoryError异常出现在日志的原因

同样通过全局搜索,可以看到ThrowOutOfMemoryError的代码位于thread.cc中,ThrowOutOfMemoryError的代码如下:

void Thread::ThrowOutOfMemoryError(const char* msg) {
  LOG(WARNING) << StringPrintf("Throwing OutOfMemoryError \"%s\"%s",
      msg, (tls32_.throwing_OutOfMemoryError ? " (recursive case)" : ""));
      
  if (!tls32_.throwing_OutOfMemoryError) {
    tls32_.throwing_OutOfMemoryError = true;
    ThrowNewException("Ljava/lang/OutOfMemoryError;", msg);
    tls32_.throwing_OutOfMemoryError = false;
  } else {
    Dump(LOG_STREAM(WARNING));
    SetException(Runtime::Current()->GetPreAllocatedOutOfMemoryError());
  }
}

可以看到对于OutOfMemoryError的处理,会首先在日志中打印出来当前的异常,同时在抛出OutOfMemoryError,所以这也是为什么就算当前异常被clear掉了,同样会在控制台打印出当前日志的原因。

总结

通过从Java层到native层的一顿分析,最后得到了无法捕获到异常的结果是由于setContentView最终是通过反射加载的相关View,在反射过程中,抛出的异常会通过native代码进行包装,使得OutOfMemoryError被包装成了InvocationTargetException,InvocationTargetException在通过层层的外抛,在Java层的creatView方法中被捕获,被包装为InflateException。

如果当前异常没有被捕获的话,还会传递到ActivityThread中,最终被包装为RuntimeException。
所以才会出现控制台中打印了RuntimeException,InfalteException,InvocationTargetException以及OutOfMemoryError的问题。

InflateException继承于RuntimeException,所以对于这里的异常,在外围可以通过Exception,InflateException或者RuntimeException进行捕获。

所以经过上述的分析,并不是异常不能被捕获,而是异常经过层层的传递,通过不同的包装,最后已经不是原来的样子,后来的我们,早已经没有了从前的样子~

问题是一个很简单的问题,但是要弄清楚问题发生的原因,对待任何的Bug都需要有肯钻研的精神,这样在以后处理类似的问题才会游刃有余,在一次次的探索中,也往往会有意想不到的发现~

End

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值