最近的问题上报平台上上报了一个OOM问题,在Activity中setContentView时候子View因为decode了较大的Drawable导致了OOM。该OOM问题是一个常规的OOM问题,但是在修改这个bug的时候发现了一个神奇的现象,外围Activity的setContentView中已经添加了Try Catch函数,但是并没有Catch住OOM异常。
查看相关的代码,在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为什么没有被捕获?
分析问题,首先要熟悉问题发生的流程,为了贴近这次问题,我们通过异常栈对整个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好像实际不能捕获到我们的异常。
分析了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, "" , "(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源码。
同样通过全局搜索,可以看到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都需要有肯钻研的精神,这样在以后处理类似的问题才会游刃有余,在一次次的探索中,也往往会有意想不到的发现~