DeadObjectException解题
RemoteException occurs on reporting focusChanged, w=Window{2470935 u0 bundle_id/bundle_id.MainActivity}
android.os.DeadObjectException
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(Binder.java:1143)
at android.view.IWindow$Stub$Proxy.windowFocusChanged(IWindow.java:500)
at com.android.server.wm.WindowState.reportFocusChangedSerialized(WindowState.java:3879)
at com.android.server.wm.WindowManagerService$H.handleMessage(WindowManagerService.java:5426)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:214)
at android.os.HandlerThread.run(HandlerThread.java:65)
at com.android.server.ServiceThread.run(ServiceThread.java:44)
Question 1: 能否try-catch住?
想要搞清楚这个问题,我们需要对try-catch
有个基础的了解。用法上面的东西,不在这里的讨论范围之内。
我们经常面临一个面试的问题。 OutOfMemoryError 可以被 try catch 吗? 对了,我不反感面试,因为面试会暴露我们知识点上的很多问题。所以,我们由这个问题来展开。
想要搞清楚这个问题。我们就要知道一些基础理论
- 什么可以被
try-catch
?
针对上面的面试题,我们就有了一下的问题。
OutOfMemoryError
可以被try-catch
吗?- 捕获
OutOfMemoryError
有什么意义? JVM
中哪一块内存不会发生OOM
?
回归到主题,针对DeadObjectException
,我们就有了这个问题。
- 当前这个
Exception
,可以被catch
住么?有用么?
Question 1.1: 什么可以被try-catch
我们可以知道,Android的可抛出类(即Throwable),可以有两个子类, 异常Exception和错误Error。而所有的Thrwoable
都可以被捕捉。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tUzs8BWS-1670401244216)(https://picx.zhimg.com/80/v2-712ccd82f388ea55eba4e8958984f479_1440w.webp)]
Exception
**异常Exception:一般指可以或应该捕获和处理的异常。**它的两个直接子类IOException
和RuntimeException
及其子类都是我们在代码中经常遇到的一些错误。AWTException
表示,抽象的窗口工具发生了异常,该Exception
,在java.awt
中抛出。java.awt
为JAVA
提供的用于创建界面以及图形绘制的软件包。而在Android
开发中,我们基本不会遇到该Exception
。
RuntimeException
是在程序运行中可能发生的异常,我们可以不捕获它,但可能带来Crash
的代价,但是过多的捕获异常又不利于暴露和调试异常情况。在开发过程中,我们更多的应该及时暴露问题。
运行时异常和受检异常
运行时异常:都是RuntimeException
类及其子类异常,如NullPointerException
(空指针异常)、IndexOutOfBoundsException
(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java
编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch
语句捕获它,也没有用throws
子句声明抛出它,也会编译通过。
非运行时异常 (编译异常):是RuntimeException
以外的异常,类型上都属于Exception
类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException
、SQLException
等以及用户自定义的Exception
异常,一般情况下不自定义检查异常。
常见异常中英对照
异常类型 | 具体显示 |
---|---|
算数异常类 | ArithmeticExecption |
空指针异常类型 | NullPointerException |
类型强制转换类型 | ClassCastException |
数组负下标异常 | NegativeArrayException |
数组下标越界异常 | ArrayIndexOutOfBoundsException |
文件已结束异常 | EOFException |
文件未找到异常 | FileNotFoundException |
字符串转换为数字异常 | NumberFormatException |
操作数据库异常 | SQLException |
输入输出异常 | IOException |
方法未找到异常 | NoSuchMethodException |
下标越界异常 | IndexOutOfBoundsExecption |
系统异常 | SystemException |
创建一个大小为负数的数组错误异常 | NegativeArraySizeException |
数据格式异常 | NumberFormatException |
安全异常 | SecurityException |
不支持的操作异常 | UnsupportedOperationException |
Error
错误 Error :一般指非正常状态的,比较严重的,不应该被捕获的系统错误。 我们常见的Error
就是OOM
和StackOverflowError
了
Question 1.2: OutOfMemoryError 可以被 try catch 吗?
根据上面所得到的信息。OOM
其实是可以被捕捉的。
Question 1.3: 捕获 OutOfMemoryError 有什么意义?
一般情况下,捕获OutOfMemoryError
并没有什么太大意义,在开发中也几乎没有写过这种代码。
如果把捕获OOM
当做处理OOM
的一种手段,无疑是不合适的。因为无法保证catch
的代码就是导致OOM
的原因,可能它只是压死骆驼的最后一根稻草,甚至也无法保证catch
代码块中不会再次触发OOM
。
Android
源码中有这样的操作。在View.java
的buildDrawingCacheImpl()
方法中有这么一段代码.
try {
bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
width, height, quality);
bitmap.setDensity(getResources().getDisplayMetrics().densityDpi);
if (autoScale) {
mDrawingCache = bitmap;
} else {
mUnscaledDrawingCache = bitmap;
}
if (opaque && use32BitCache) bitmap.setHasAlpha(false);
} catch (OutOfMemoryError e) {
// If there is not enough memory to create the bitmap cache, just
// ignore the issue as bitmap caches are not required to draw the
// view hierarchy
if (autoScale) {
mDrawingCache = null;
} else {
mUnscaledDrawingCache = null;
}
mCachingFailed = true;
buildDrawingCacheImpl()
方法的大致作用是为当前View
生成一个Bitmap
缓存。在构建Bitmap
对象的时候,如果捕捉到了OOM
,就放弃生成Bitmap
缓存,因为在View
的绘制过程中Bitmap Cache
并不是必须存在的。所以在这里没有必要抛出OOM
,而是自己捕获就可以了。
在你自己明确知道可能发生OOM
的情况下设置一个兜底策略,这可能是捕获OOM
的唯一意义了.
Question 1.4: JVM中哪一块内存不会发生OOM?
要了解这个问题。需要先了解JVM模型是什么样的。
简单版本如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qRnVfXG7-1670401244217)(https://picx.zhimg.com/80/v2-279cac84feaaca095a49f75f19f76a47_1440w.webp)]
还有一个终极版本:
Java虚拟机栈
每个方法被执行的时候,Java
虚拟机栈都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError
异常。如果 Java
虚拟机栈支持动态扩展,当栈扩展时无法申请到足够的内存会排抛出OutOfMemoryError
异常。
本地方法栈
本地方法栈为虚拟机使用到的Native
方法服务。《Java 虚拟机规范》对本地方法栈中方法使用的语言、使用方式和数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它。
Hotspot
将本地方法栈和虚拟机栈合二为一。本地方法栈也会在栈深度溢出和栈扩展失败时分别抛出 StackOverflowError
和OutOfMemoryError
。
Java 堆
所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java
世界里 “几乎” 所有的对象实例都在这里分配内存。
在 《Java 虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”。Java 堆以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。
Java 堆既可以被实现成固定大小,也可以是扩展的。如果在Java
堆中没有内存完成实例分配,并且堆无法再扩展时Java
虚拟机将会抛出OutOfMemoryError
。
方法区
方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。Hotspot
设计之初选择把垃圾收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,使得HotSpot
的 GC 能够像管理 Java 堆一样管理这部分内存,但导致 Java 应用更容易遇到内存溢出的问题。
在 JDK 8 中,彻底废弃了永久代的概念。如果方法区无法满足新的内存分配的需求时,将抛出 OutOfMemoryError 。
运行时常量池。
方法区的一部分。Class 文件的常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后方法方法去的运行时常量池。运行时常量池具有动态性,运行期间也可以将新的常量放入池中,如 String.intern() 。常量池受到方法区的限制,当无法再申请到内存时,会抛出 OutOfMemoryError 。
Question 1.5: 当前这个Exception,可以被catch住么?有用么?
我们从上面的只是可以知道,DeadObjectException
可以被catch
住。但是有用么?也是要具体问题具体分析的。如果报在我们自己的代码里,try-catch
是有用的。
结论
首先我们要来看,在哪里报错的错
ServiceThread
我在Android
的frameworks
里面,一个不知名的版本中,找到了这样的一个注释。
/**
* Special handler thread that we create for system services that require their own loopers.
*/
public class ServiceThread extends HandlerThread
其实这玩意就是,一个给需要有一个Lopper
的系统服务用的HandlerThread
。
所以
这玩意是个系统服务。要通过Binder
来给我们的App
分发windowFocusChanged
的时候报错了。首先,我们肯定无法去底层做try-catch
的。而且,我也相信,这一定是做了try-catch
的。
下面,我们就要讨论,如果在引发点,来做try-catch
有没有用。其实是没用的。因为,对于我们的程序来说,接收到用户的操作,给系统发送请求。引发了页面变化。对我们的程序来说,try-catch
只到通过Binder
调用远端的接口。这个错误报在里远端的Looper
里,对我们来说,已经圆满完成任务了。
所以,try-catch
对于这个报错来说,在我们自己代码里是无用的。
Question 2: 这个BUG是怎么引起的?
我们就要看懂这个报错,就要知道,这个BUG是怎么引起的。什么时候会爆这个错。这个BUG是报在了windowFocusChanged
过程中。所以,我们要了解Activity.onWindowFocusChanged()
的调用流程是怎样的。并且,用法是什么样的。
Activity.onWindowFocusChanged 用法
方法原型如下:
public void onWindowFocusChanged (boolean hasFocus)
触发onWindowFocusChanged
的情况有多种,比如应用前后台来回切换、软键盘弹出或者隐藏、首次进入一个Activity
后会在onResume
方法之后调用等。
那么,这个方法有什么用处呢?
Activity
生命周期中,onStart
, onResume
, onCreate
都不是真正visible
的时间点, 真正的visible
时间点是onWindowFocusChanged()
函数被执行时。从onWindowFocusChanged
被执行起,用户可以与应用进行交互了, 而这之前,对用户的操作需要做一点限制,常见的就是测量控件,获取控件的宽高。
如果我们不用
onWindowFocusChanged
怎么获取View宽高?延后调用?监听Layout?
Activity.onWindowFocusChanged 调用过程
我们以ActivityThread.handleResumeActivity()方法作为切入点开始分析。
public final class ActivityThread extends ClientTransactionHandler
//省略其他方法
...
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
//省略部分代码
...
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
final Activity a = r.activity;
//省略部分代码
...
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
wm.addView(decor, l);
//省略部分代码
..