RxJava 异常时堆栈显示不正确?解决方法都在这里

yizhihongxing

本文首发我的博客,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。

image-20211122164509577

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")
        }
    }

image-20211122170335525

原理

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#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 已通过

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

原文链接:https://www.cnblogs.com/gdutxiaoxu/p/17137185.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:RxJava 异常时堆栈显示不正确?解决方法都在这里 - Python技术站

(0)
上一篇 2023年4月18日
下一篇 2023年4月18日

相关文章

  • Android WebView 踩坑日记,字体怎么突然变小了???

    背景 最近,端内在做 webView 统一的时候,个性签名中的 WebView 替换为 CustomWebView 之后,发现字体突然变小。 一开始不知道是什么原因,通过二分法查找最近的提交,排查之后,发现是 SignatureWebView 的继承关系从 WebView 修改为 CustomWebView。revert 之后就正常了。 于是,我问自己,为什…

    Android 2023年4月18日
    00
  • Android报”StaleDataException”如何解决?

    StaleDataException 是一个非常常见的 Android 异常,它在我们开发中经常会遇到。它的含义是:数据已经过期了。在使用 Cursor 的时候,有可能会遇到这个异常,主要原因是由于在 Cursor 有效时执行了更新/删除操作,导致 Cursor 中的数据已经过期,无法进行操作。下面我将详细讲解这个异常的原因和解决办法。 原因 当我们使用 C…

    Android 2023年4月3日
    00
  • 音频编辑服务UI SDK接入指导及常见问题

    华为 HMS Core 音频编辑服务(Audio Editor Kit)是华为帮助全球开发者快速构建各类应用音频能力的服务,汇聚了华为在音乐、语音等相关音频领域的先进技术。音频编辑服务为开发者们提供音频基础编辑、AI配音、音源分离、空间渲染、变声、多种音效等丰富的音频处理能力,以及性能优异、简单易用、开放性强的接口,开发者们可依据应用场景,在App中高效轻松…

    Android 2023年4月18日
    00
  • Fragment回退栈相关操作

    在我的场景里,会创建多个Fragment,Fragment之间可以互相跳转,点击返回键需要一级一级往上返回。因此需要一个类似于Activity的回退栈,当然没必要做到Activity那么复杂,满足先进先出的效果即可。 添加Fragment回退栈 添加个Fragment,并将其加入回退栈,代码如下: FragmentManager fragmentManage…

    Android 2023年4月18日
    00
  • 这种开发方式你了解吗?

    随着移动互联网的发展,移动应用程序的需求越来越高,而原生应用程序的开发成本和时间较高,导致一些企业选择采用H5技术构建应用程序。 但是,H5技术在性能、用户体验、功能等方面仍有局限性,因此,有些企业转而选择「hybrid + 小程序」技术架构来构建应用程序。 相对于H5应用程序,小程序在用户体验、性能、功能等方面有很多优势。首先,小程序不需要像H5应用程序那…

    Android 2023年4月25日
    00
  • 游戏内嵌社区服务开放,助力开发者提升玩家互动与留存

    华为 HMS Core 游戏内嵌社区服务提供快速访问华为游戏中心论坛能力,支持玩家直接在游戏内浏览帖子和交流互动,助力开发者扩展内容生产和触达的场景。 一、为什么要游戏内嵌社区? 二、游戏内嵌社区的典型使用场景 1、游戏内打开论坛 您可以在游戏内绘制论坛入口,为玩家提供沉浸式发帖、浏览、点赞、回帖、参与投票等各类互动体验。 说明:您可根据游戏风格,自定义论坛…

    Android 2023年4月18日
    00
  • Android Studio中的一些常见控件

    Android Studio是一款非常流行的用于开发Android应用程序的集成开发环境(IDE)。它提供了许多内置控件,使开发人员可以轻松创建应用程序界面和功能。在本文中,我们将介绍Android Studio中的一些常见控件,例如TextView,Button,EditText,ImageView等。 TextView控件 TextView是一个用于显示…

    Android 2023年4月17日
    00
  • Android报”SecurityException”如何解决?

    首先我们需要知道在Android中,每个应用程序都会运行在自己的Sandbox中,这是为了保证应用程序之间的安全性和隔离性。这意味着当我们试图从应用程序中访问另一个应用程序或系统的一些敏感资源时,我们可能会遇到”SecurityException”异常。 该异常表示当前的应用程序没有足够的权限来执行某个操作。通常可以通过以下两种方式来解决该问题: 申请相关权…

    Android 2023年4月3日
    00
合作推广
合作推广
分享本页
返回顶部