Android RxJava 异常时堆栈信息显示不全(不准确),解决方案都在这里了
现象
大家好,我是徐公,今天为大家带来的是 RxJava 的一个血案,一行代码 return null 引发的。
前阵子,组内的同事反馈说 RxJava 在 debug 包 crash 了,捕获到的异常信息不全。(即我们捕获到的堆栈没有包含我们自己代码,都是一些系统或者 RxJava 框架的代码)
典型的一些 error 信息如下:
io.reactivex.exceptions.OnErrorNotImplementedException: The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | java.lang.NullPointerException: Callable returned null
at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704)
at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701)
at io.reactivex.internal.observers.LambdaObserver.onError(LambdaObserver.java:77)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.checkTerminated(ObservableObserveOn.java:281)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.drainNormal(ObservableObserveOn.java:172)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.run(ObservableObserveOn.java:255)
at io.reactivex.android.schedulers.HandlerScheduler$ScheduledRunnable.run(HandlerScheduler.java:124)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7682)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:516)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
Caused by: java.lang.NullPointerException: Callable returned null
at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39)
at io.reactivex.internal.operators.observable.ObservableFromCallable.subscribeActual(ObservableFromCallable.java:43)
at io.reactivex.Observable.subscribe(Observable.java:12267)
at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96)
at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578)
at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
可以看到,上面的 Error 堆栈信息中,它并没有给出这个 Error 在实际项目中的调用路径。可以看到,报错的堆栈,提供的有效信息较少, 我们只能知道是由于 callable.call() 这里返回了 Null,导致出错。却不能判断 callable 是哪里创建的,这时候我们只能结合日志上下文,判断当前之前的代码大概在哪里,再逐步排查。
public final class ObservableFromCallable<T> extends Observable<T> implements Callable<T> {
@Override
public void subscribeActual(Observer<? super T> observer) {
DeferredScalarDisposable<T> d = new DeferredScalarDisposable<T>(observer);
observer.onSubscribe(d);
if (d.isDisposed()) {
return;
}
T value;
try {
// callable.call() 这里返回了 Null,并传递给了 RxJavaPlugins 的 errorHandler
value = ObjectHelper.requireNonNull(callable.call(), "Callable returned null");
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
if (!d.isDisposed()) {
observer.onError(e);
} else {
RxJavaPlugins.onError(e);
}
return;
}
d.complete(value);
}
}
一顿操作猛如虎,很多,我们结合一些让下文日志,发现是这里返回了 null,导致出错
backgroundTask(Callable<Any> {
Log.i(TAG, "btn_rx_task: ")
Thread.sleep(30)
return@Callable null
})?.subscribe()
/**
* 创建一个rx的子线程任务Observable
*/
private fun <T> backgroundTask(callable: Callable<T>?): Observable<T>? {
return Observable.fromCallable(callable)
.compose(IOMain())
}
如果遇到 callable 比较多的情况下,这时候 一个个排查 callable,估计搞到你吐血。
那有没有什么较好的方法,比如做一些监控?完整打印堆栈信息。
第一种方案,自定义 Hook 解决
首先,我们先来想一下,什么是堆栈?
在我的理解里面,堆栈是用来储存我们程序当前执行的信息。在 Java 当中,我们通过 java.lang.Thread#getStackTrace 可以拿到当前线程的堆栈信息,注意是当前线程的堆栈。
而 RxJava 抛出异常的地方,是在执行 Callable#call 方法中,它打印的自然是 Callable#call 的方法调用栈,而如果 Callable#call 的调用线程跟 callable 的创建线程不一致,那肯定拿不到 创建 callable 时候的堆栈。
而我们实际上需要知道的是 callable 创建的地方,对应到我们我们项目报错的地方,那自然是 Observable.fromCallable 方法的调用栈。
这时候,我们可以采用 Hook 的方式,来 Hook 我们的代码
为了方便,我们这里采用了 wenshu 大神的 Hook 框架, github, 想自己手动去 Hook 的,可以看一下我两年前写的文章 Android Hook 机制之简单实战,里面有介绍介绍一些常用的 Hook 手段。
很快,我们写出了如下代码,对 Observable#fromCallable 方法进行 hook
fun hookRxFromCallable() {
// DexposedBridge.findAndHookMethod(ObservableFromCallable::class.java, "subscribeActual", Observer::class.java, RxMethodHook())
DexposedBridge.findAndHookMethod(
Observable::class.java,
"fromCallable",
Callable::class.java,
object : XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam?) {
super.beforeHookedMethod(param)
val args = param?.args
args ?: return
val callable = args[0] as Callable<*>
args[0] = MyCallable(callable = callable)
}
override fun afterHookedMethod(param: MethodHookParam?) {
super.afterHookedMethod(param)
}
})
}
class MyCallable(private val callable: Callable<*>) : Callable<Any> {
private val TAG = "RxJavaHookActivity"
val buildStackTrace: String?
init {
buildStackTrace = Rx2Utils.buildStackTrace()
}
override fun call(): Any {
Log.i(TAG, "call: ")
val call = callable.call()
if (call == null) {
Log.e(TAG, "call should not return null: buildStackTrace is $buildStackTrace")
}
return call
}
}
再次执行我们的代码
backgroundTask(Callable<Any> {
Log.i(TAG, "btn_rx_task: ")
Thread.sleep(30)
return@Callable null
})?.subscribe()
可以看到,当我们的 Callable 返回为 empty 的时候,这时候报错的信息会含有我们项目的代码, perfect。
第二种方案,RxJavaExtensions
最近,在 Github 上面发现了这一个框架,它也可以帮助我们解决 RxJava 异常过程中信息不全的问题。它的基本使用如下:
使用
https://github.com/akarnokd/RxJavaExtensions
第一步,引入依赖库
dependencies {
implementation "com.github.akarnokd:rxjava2-extensions:0.20.10"
}
第二步:先启用错误追踪:
RxJavaAssemblyTracking.enable();
第三步:在抛出异常的异常,打印堆栈
/**
* 设置全局的 onErrorHandler。
*/
fun setRxOnErrorHandler() {
RxJavaPlugins.setErrorHandler { throwable: Throwable ->
val assembled = RxJavaAssemblyException.find(throwable)
if (assembled != null) {
Log.e(TAG, assembled.stacktrace())
}
throwable.printStackTrace()
Log.e(TAG, "setRxOnErrorHandler: throwable is $throwable")
}
}
原理
RxJavaAssemblyTracking.enable();
public static void enable() {
if (lock.compareAndSet(false, true)) {
// 省略了若干方法
RxJavaPlugins.setOnObservableAssembly(new Function<Observable, Observable>() {
@Override
public Observable apply(Observable f) throws Exception {
if (f instanceof Callable) {
if (f instanceof ScalarCallable) {
return new ObservableOnAssemblyScalarCallable(f);
}
return new ObservableOnAssemblyCallable(f);
}
return new ObservableOnAssembly(f);
}
});
lock.set(false);
}
}
可以看到,它调用了 RxJavaPlugins.setOnObservableAssembly 方法,设置了 RxJavaPlugins onObservableAssembly 变量
而我们上面提到的 Observable#fromCallable 方法,它里面会调用 RxJavaPlugins.onAssembly 方法,当我们的 onObservableAssembly 不为 null 的时候,会调用 apply 方法进行转换。
public static <T> Observable<T> fromCallable(Callable<? extends T> supplier) {
ObjectHelper.requireNonNull(supplier, "supplier is null");
return RxJavaPlugins.onAssembly(new ObservableFromCallable<T>(supplier));
}
public static <T> Observable<T> onAssembly(@NonNull Observable<T> source) {
Function<? super Observable, ? extends Observable> f = onObservableAssembly;
if (f != null) {
return apply(f, source);
}
return source;
}
因此,即当我们设置了 RxJavaAssemblyTracking.enable(), Observable#fromCallable 传递进来的 supplier,最终会包裹一层,可能是 ObservableOnAssemblyScalarCallable,ObservableOnAssemblyCallable,ObservableOnAssembly。典型的装饰者模式应用,这里不得不说,RxJava 对外提供的这个点,设计得真巧妙,可以很方便我们做一些 hook。
我们就以 ObservableOnAssemblyCallable 看一下
final class ObservableOnAssemblyCallable<T> extends Observable<T> implements Callable<T> {
final ObservableSource<T> source;
// 将在哪里创建的 Callable 的堆栈信息保存下来
final RxJavaAssemblyException assembled;
ObservableOnAssemblyCallable(ObservableSource<T> source) {
this.source = source;
this.assembled = new RxJavaAssemblyException();
}
@Override
protected void subscribeActual(Observer<? super T> observer) {
source.subscribe(new OnAssemblyObserver<T>(observer, assembled));
}
@SuppressWarnings("unchecked")
@Override
public T call() throws Exception {
try {
return ((Callable<T>)source).call();
} catch (Exception ex) {
Exceptions.throwIfFatal(ex);
throw (Exception)assembled.appendLast(ex);
}
}
}
public final class RxJavaAssemblyException extends RuntimeException {
private static final long serialVersionUID = -6757520270386306081L;
final String stacktrace;
public RxJavaAssemblyException() {
this.stacktrace = buildStackTrace();
}
}
可以看到,他是直接在 ObservableOnAssemblyCallable 的构造方法的时候,直接将 Callable 的堆栈信息保存下来,类为 RxJavaAssemblyException。
而当 error 报错的时候,调用 RxJavaAssemblyException.find(throwable) 方式,判断是不是 RxJavaAssemblyException,是的话,直接返回。
public static RxJavaAssemblyException find(Throwable ex) {
Set<Throwable> memory = new HashSet<Throwable>();
while (ex != null) {
if (ex instanceof RxJavaAssemblyException) {
return (RxJavaAssemblyException)ex;
}
if (memory.add(ex)) {
ex = ex.getCause();
} else {
return null;
}
}
return null;
}
到这里,RxJavaAssemblyTracking 能将 error 信息完整打印出来的流程已经讲明白了,其实就是在创建 Callable 的时候,采用一个包装类,在构造函数的时候,将 error 信息报错下来,等到出错的时候,再将 error 信息,替换成保存下来的 error信息。
我们的自定义 Hook 也是利用这种思路,提前将 callable 创建的堆栈暴露下来,换汤不换药。
一些思考
上述的方案我们一般不会带到线上,为什么呢? 因为对于每一个 callable,我们需要提前保存堆栈,而获取堆栈是耗时的。那有没有什么方法呢?
如果项目有接入 Matrix 的话,可以考虑借用 Matrix trace 的思想,因为在方法前后插入 AppMethodBeat#i 和 AppMethodBeat#o 这样当我们执行方法的时候,因为插桩了,我们可以方便得获取到方法执行耗时,以及方法的调用栈。
// 第一步:需要在合适的实际先生成 beginRecord
AppMethodBeat.IndexRecord beginRecord = AppMethodBeat.getInstance().maskIndex("AnrTracer#dispatchBegin");
// 第二步:方法的调用栈信息在 data 里面
long[] data = AppMethodBeat.getInstance().copyData(beginRecord);
第三步:
将 data 转化为我们想要的 stack(初步看了代码,需要我们修改 trace 的代码)
参考资料
rxjava-2-doesnt-tell-the-error-line
how-to-log-a-stacktrace-of-all-exceptions-of-rxjava2
推荐阅读
腾讯 Matrix 增量编译 bug 解决之路,PR 已通过
Android RxJava 异常时堆栈信息显示不全(不准确),解决方案都在这里了的更多相关文章
- RxJava 异常时堆栈显示不正确?解决方法都在这里
本文首发我的博客,github 地址 大家好,我是徐公,今天为大家带来的是 RxJava 的一个血案,一行代码 return null 引发的. 前阵子,组内的同事反馈说 RxJava 在 debug ...
- Android 启动APP时黑屏白屏的解决方案
在开发中,我们在启动app的时候,屏幕会出现一段时间的白屏或者黑屏,不同设备时间长短不同.很影响用户体验. 首先分析一下,产生这个现象的原因,当我们在启动一个应用时,系统会去检查是否已经存在这样一个进 ...
- JAVA将异常的堆栈信息转成String
有时候我们需要将系统出现异常的堆栈信息显示到异常页面的一个隐藏的DIV内,这样查看源时就可以快速的定位到异常信息.这个时候就要将异常信息转成String. /* * 将异常的堆栈信息转成String ...
- Android编译异常` Cause: duplicate entry: META-INF/MANIFEST.MF`
目录 Android编译异常Cause: duplicate entry: META-INF/MANIFEST.MF 解决方案2: 治标又治本(暂无) 解决方案1: 治标不治本(还原大法) 参考资料 ...
- IE8 下 select option 内容过长 , 展开时信息显示不全解决办法
IE8 下 select option 内容过长 , 展开时信息显示不全 , 简单折衷的方式就是给 option 加上 title 属性 , 但是又不想一个个的修改,怎么办呢,代码如下 : //sel ...
- [转]Android下打印调试堆栈方法
http://blog.csdn.net/freshui/article/details/9456889 打印堆栈是调试的常用方法,一般在系统异常时,我们可以将异常情况下的堆栈打印出来,这样十分方便错 ...
- 深入Android RxJava 2
这篇文章是根据Jake Wharton在GOTO CopenHagen 2016上的讲话整理的. 下一个版本(2.0)的RxJava还在开发中.虽然observable.订阅管理和背压(backpre ...
- 异常 Exception 堆栈跟踪 异常捕获 MD
Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...
- 编写高质量代码改善C#程序的157个建议[用抛异常替代返回错误、不要在不恰当的场合下引发异常、重新引发异常时使用inner Exception]
前言 自从.NET出现后,关于CLR异常机制的讨论就几乎从未停止过.迄今为止,CLR异常机制让人关注最多的一点就是“效率”问题.其实,这里存在认识上的误区,因为正常控制流程下的代码运行并不会出现问题, ...
- [转]Android在初始化时弹出popwindow的方法 .
转自:http://blog.csdn.net/sxsboat/article/details/7340759 留个人备用0.0 Android中在onCreate()时弹出popwindow,很多人 ...
随机推荐
- .NET8.0 AOT 经验分享 - 专项测试各大 ORM 是否支持
AOT 特点 发布和部署本机 AOT 应用具有以下优势: 最大程度减少磁盘占用空间:使用本机 AOT 发布时,将生成一个可执行文件,其中仅包含支持程序所需的外部依赖项的代码.减小的可执行文件大小可能会 ...
- 字节跳动AB实验经验分享:企业如何构建数据驱动的实验文化?
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 近日,CCF TF 举办了第 123 期分享活动,本期主题为"用户体验工程". CCF TF ...
- C语言从键盘上输入年份和月份,计算并输出这一年的这一月共有多少天。
#include<stdio.h> void main() { int y, n, s = 0;//定义变量 scanf_s("%d-%d", &y, & ...
- C语言输入任意长度数组后,再在该数组中查找特定的值,并且可查找多个相同的值
C语言输入任意长度数组后,再在该数组中查找特定的值,并且可查找多个相同的值 例:在a[20] = { 99,42,57,74,46,85,32,78,40,33,74,88,65,27,38,69,5 ...
- JAVAweek7
本周学习[函数][数组] 什么是函数: 函数就是定义在类中的具有特定功能的一段独立小程序.函数也称为方法. 函数的格式: ·修饰符 返回值类型 函数名(参数类型 形式参数) { 执行语句: retur ...
- 【Android】打卡app 今日完成情况记录
已完成内容:页面的设计.数据库的建立 今日目标:实现数据库的增删改查,并能够在页面实现输出 逻辑: 1.用户进入注册页面,进行注册,注册成功后,可以成功登录(这一阶段就完成了) 2.登录后,显示用户的 ...
- python中的post请求
用python来验证接口正确性,主要流程有4步: 1 设置url 2 设置消息头 3 设置消息体 4 获取响应 5 解析相应 6 验证数据 Content-Type的格式有四种:分别是applicat ...
- 0x02.加密和编码
识别算法编码类型 看密文位数 看密文特征(数字.字母.大小写.符号等) 看当前密文存在的地方(web.数据库.操作系统等) 密码存储加密 md5:16位和32位由0-9和a-f组成的字符串 ,该加密方 ...
- K8s 里多容器 Pod 的健康检查探针工作机制分析
目录 1. 开篇 2. 聊啥 3. 结论(TL;DR) 4. 测试过程 4.1 准备测试用镜像 4.2 准备 Deployment YAML 4.3 准备 Service YAML 4.4 准备第二个 ...
- 使用IDEA2022.3创建web工程~
为什么突然记录这么一篇博客呢? 以前都是用2019IDEA的,突然换成了IDEA2022懵逼了,所以记录一下~ 具体步骤 1.创建一个新的Project 2.注意选择BuildSystem 3.在当前 ...