RxJava 异常时堆栈显示不正确?解决方法都在这里
本文首发我的博客,github 地址
大家好,我是徐公,今天为大家带来的是 RxJava 的一个血案,一行代码 return null 引发的。
前阵子,组内的同事反馈说 RxJava 在 debug 包 crash 了,捕获到的异常信息不全。(即我们捕获到的堆栈没有包含我们自己代码,都是一些系统或者 RxJava 框架的代码)
典型的一些 error 信息如下:
可以看到,上面的 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 已通过
我是站在巨人的肩膀上成长起来的,同样,我也希望成为你们的巨人。觉得不错的话可以关注一下我的微信公众号徐公。
RxJava 异常时堆栈显示不正确?解决方法都在这里的更多相关文章
- Saiku图表导出时中文显示问题的解决方法
Saiku图表导出时png,jpg,pdf三种格式的中文显示都有问题,目前找到一种不太完善的解决方法(中文可以显示但不清晰),需要修改Saiku项目下的ExporterResource.java文件, ...
- 升级或者重装Discuz! 版本后 QQ互联英文乱码显示的正确解决方法
升级Discuz! X3版本QQ互联英文乱码!connect_viewthread_share_to_qq! 目前Discuz!论坛上 最简单的解决方法: 第一步:后台----->站长---- ...
- NestedScrollView嵌套ListView时只显示一行的解决方法
在使用CoordinatorLayout和AppBarLayout实现嵌套滑动的时候,出现listview没有嵌套滑动: 如果要实现嵌套滑动,则需要添加NestedScrollView,但是结果发现l ...
- linux下安装Oracle时交换空间不足的解决方法
摘:linux下安装Oracle时交换空间不足的解决方法 linux上安装Oracle时交换空间不足的解决办法 增加交换空间有两种方法: 严格的说,在系统安装完后只有一种方法可以增加swap,那就是本 ...
- vue打包后显示空白正确处理方法
vue打包后显示空白正确处理方法是 1.找到配置文件(js与css加载不上) 修改 这样打包处理可以打开但是页面样式会找不到 2.修改(针对css中的图片加载不上) 找到对应的位置加上publicPa ...
- ubuntu下mysqli_connect()显示未定义,mysqli_fetch_all()显示未定义 解决方法
mysqli_connect()显示未定义解决方法: http://www.cnblogs.com/misoag/archive/2013/01/24/2874439.html 让apache.php ...
- photoshop菜单显示不全的解决方法
photoshop菜单显示不全? 解决方法,选择菜单 编辑->菜单,下拉菜单选择photoshop默认值
- [转]MySQL忘记密码的正确解决方法
http://database.51cto.com/art/201005/201986.htm 以下的文章主要介绍的是MySQL忘记密码的正确解决方法,在实际操作中如果你忘记MySQL密码是一件很头痛 ...
- SVN 不显示状态图标--解决方法
[SVN 不显示状态图标--解决方法] 在名字前面加空格, 三个六个空格随意: 参考:https://www.cnblogs.com/lzpong/p/6187366.html
- 使用WebLogic时控制台输出中文乱码解决方法
使用WebLogic时控制台输出中文乱码解决方法 1.找到weblogic安装目录,当前项目配置的domain 2.找到bin下的setDomainEnv.cmd文件 3.打开文件,从文件最后搜索第一 ...
随机推荐
- layui的table数据匹配问题
<script> layui.use('table', function () { var table = layui.table; //第一个实例 table.render({ elem ...
- services资源+pod详解
services资源+pod详解 一.Service 虽然每个Pod都会分配一个单独的Pod IP,然而却存在如下两问题: Pod IP 会随着Pod的重建产生变化 Pod IP 仅仅是集群内可见的虚 ...
- 18V降压3.3V,15V降压3.3V的降压IC和LDO芯片方案
在 18V 和 15V 输入中,我们需要给其他电源电路提高供电,有的电路的供电电压在 5V,或者是 3.3V 时, 我们就需要使用降压芯片来组建一个降压电路来给后面的的电路,提供稳定的,持续的 3.3 ...
- 5V升压12.6V芯片电路图,三节锂电池充电
三节3.7V的锂电池串联,11.1V和最大12.6V锂电池充电电路的解决方案.在应用中,一般使用低压5V,如USB口直接输入的给三串锂电池充电,还有是15V或者18V,20V输入降压给锂电池充电的两种 ...
- Vue3 企业级优雅实战 - 组件库框架 - 8 搭建组件库 cli
前面的文章分享了组件库的开发.example.组件库文档,本文分享组件库 cli 开发. 1 为什么要开发组件库 cli 回顾一个新组件的完整开发步骤: 1 在 packages 目录下创建组件目录 ...
- PHP-表单传值
一.传值引入 了解传值必须要先知道为什么需要传值? 传值的主要作用是为了实现用户数据的定制化,用户与服务端的互交 二.传值的方式 虽然 http协议中有很多数据传输的方式,但在PHP中只有 POST ...
- 实现 .Net 7 下的数据库定时检查
在软件开发过程中,有时候我们需要定时地检查数据库中的数据,并在发现新增数据时触发一个动作.为了实现这个需求,我们在 .Net 7 下进行一次简单的演示. PeriodicTimer .Net 6 中新 ...
- 让 Win8.1 微软拼音新体验模式支持 Metro 应用
内容回顾: [1]十个步骤找回 Win8 中的微软拼音新体验模式 [2]发布个工具,一键恢复Win8/8.1中的微软拼音长句模式(新体验模式) 在 [1] 中,我们找回了 Win8 里的微软拼音新体验 ...
- P1005 [NOIP2007 提高组] 矩阵取数游戏
题目传送门 前言 今天依旧是不写高精的一天呢!(是的,这位作者又只拿了开 \(LL\) 的 \(\color{yellow}{60}\) 分) 思路描述 看到数据 \(n,m \le 80(30)\) ...
- 01-Verilog基础
Verilog RTL编程实践 在进行数字IC设计过程中,RTL coding能力是非常重要的.结合逻辑仿真(VCS)和逻辑综合(Design Compiler)工具.看RTL. 1 ASIC Des ...