本文首发我的博客,github 地址

文章目录

    • 现象
    • 第一种方案,自定义 Hook 解决
    • RxJavaExtensions
      • 使用
      • 原理
    • 一些思考
    • 参考资料
  • 推荐阅读

现象

大家好,我是徐公,今天为大家带来的是 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 nullat 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 nullat 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> {@Overridepublic 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 的 errorHandlervalue = 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?.argsargs ?: returnval 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>() {@Overridepublic 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();}@Overrideprotected void subscribeActual(Observer<? super T> observer) {source.subscribe(new OnAssemblyObserver<T>(observer, assembled));}@SuppressWarnings("unchecked")@Overridepublic 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#iAppMethodBeat#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

推荐阅读

我的 5 年 Android 学习之路,那些年一起踩过的坑

职场上这四件事,越早知道越好

腾讯 Matrix 增量编译 bug 解决之路,PR 已通过

我是站在巨人的肩膀上成长起来的,同样,我也希望成为你们的巨人。觉得不错的话可以关注一下我的微信公众号徐公

  1. 公众号徐公回复黑马,获取 Android 学习视频
  2. 公众号徐公回复徐公666,获取简历模板,教你如何优化简历,走近大厂
  3. 公众号徐公回复面试,可以获得面试常见算法,剑指 offer 题解
  4. 公众号徐公回复马士兵,可以获得马士兵学习视频一份
  5. 公众号徐公回复Java 电子书,可以获得我精心整理的 Java 电子数据

希望我们可以成为朋友,成长路上的忠实伙伴!

RxJava2 堆栈信息显示不全解决方案相关推荐

  1. Javascript中递归造成的堆栈溢出及解决方案

    Javascript中递归造成的堆栈溢出及解决方案 参考文章: (1)Javascript中递归造成的堆栈溢出及解决方案 (2)https://www.cnblogs.com/cuew1987/p/4 ...

  2. IE8 下 select option 内容过长 , 展开时信息显示不全问题解决办法

    为什么80%的码农都做不了架构师?>>>    /*** IE8 下 select option 内容过长 , 展开时信息显示不全 , 简单折衷的方式就是给 option 加上 ti ...

  3. NoteExpress文献导入,信息显示不全的问题

    NoteExpress文献导入 信息显示不全 问题描述: 利用NoteExpress导入文献全文,但是发现有的文献的作者.时间.期刊信息等无法显示,如图所示. 可以看到,这两个参考文献的年份.作者等都 ...

  4. Mathtype公式Word显示不全解决方案

    问题:将Mathtype里面编辑完公式拷贝到Word里面,出现公式显示不全的现象. 解决方案:选择Office Word2007菜单栏的页面布局 -> 打开段落设置窗口 -> 将行距从固定 ...

  5. 解决 Cycript 信息显示不全的问题

    通过 cycript 注入进程,打印出 UI 结构信息,但是有时会发现显示信息不全,如下所示: # cycript -p Cydia cy# UIApp.keyWindow.recursiveDesc ...

  6. React Native Text 组件显示不全解决方案

    github上此问题的讨论 传送门 BUG 体现效果(左出问题安卓真机,右ios模拟器) 设置 minWidth 无法自动撑开 View,除非 minWidth 设置得大些 发生场景 这个解决方法不一 ...

  7. ppt文本框显示缺字,信息显示不全

    1.在文字后面加个空格. 2.重新调整文本框大小 3.不需要打开ppt,而是在图标上右键属性-摘要,把标题.主题.作者等描述全部清空或改成与本机用户一至即可.因为文本框少子是信息不匹配,就像歌曲在mp ...

  8. JScrollPane中添加组件时滚动条不出现或添加组件显示不全解决方案

    问题: 往滚动条容器里添加容器,再往容器中添加组件,当组件的大小超过滚动条的大小时,滚动条还不显现的解决办法.滚动条容器,在NEW的时候只能传一个容器进去.并且这个容器(zPanel)不需要设置大小. ...

  9. Android ListView item信息显示不全

    Bug:本人使用Android listview去显示排名分数,item的条数是6~8个,每添加一条item就排序一次并调用 notifyDataSetChanged()去刷新ListView,最后发 ...

最新文章

  1. 弹性碰撞后速度方向_高三期中考后分析,不得不知道的重难点
  2. 10 Redis 主从复制
  3. 【Linux系统编程】线程池
  4. 来自Java空间的传送门
  5. Java网络爬虫该如何学习
  6. modelsim和matlab联合仿真,Modelsim与Matlab联合仿真
  7. 公众号文章发送pdf文件 pdf文件如何变成导出图片
  8. ThinkPad工程师回答
  9. 深入浅出 Python Descriptors / Properties
  10. 探索性测试ET(Exploratory Test)
  11. breakpoint()
  12. 给Unity开发者的C#内存管理(第一部分) C# Memory Management for Unity Developers (part 1 of 3)
  13. Python绘制双曲线
  14. Spring Boot 项目鉴权的 4 种方式
  15. 如何写出好的Java代码
  16. SSD固态硬盘颗粒SLC MLC TLC QLC有什么区别
  17. uniapp使用swiper根据动态数据实现分页
  18. nginx 下载安装及进行负载均衡配置
  19. 考研数学常用基础知识
  20. 上海计算机一级选择题库及答案,2016年上海计算机一级试题及答案

热门文章

  1. docker logs使用
  2. 开源项目 - 电子签章(移动端签名方案)
  3. 田野调查手记·浮山摩崖石刻(九)
  4. 《读九章算术学Python》如何用Python编程实现盈不足术?附图解分析、代码实现和习题解答
  5. STM32F4xx系列使用HAL库配置SPI-读写FLASH
  6. 中国传统文化2022秋雨课堂期末测试答案
  7. darknet 框架 安装编译
  8. python search用法,Python-re中search()函数的用法详解(查找ip)
  9. iOS调试_Couldn't load project
  10. CISSP 报名保姆级教程