RxJava stack exception information is not fully displayed. What's wrong

Posted by keyurshah on Thu, 17 Feb 2022 09:55:13 +0100

Original text: Xu Gong

phenomenon

Hello, everyone. Today we bring you a murder case of RxJava, which is caused by a line of code return null.

A while ago, colleagues in the group reported that RxJava crash ed in the debug package, and the exception information captured was incomplete. (that is, the stack we captured does not contain our own code, but the code of some systems or RxJava framework)

Some typical error information are as follows:

 1 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
 2     at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:704)
 3     at io.reactivex.internal.functions.Functions$OnErrorMissingConsumer.accept(Functions.java:701)
 4     at io.reactivex.internal.observers.LambdaObserver.onError(LambdaObserver.java:77)
 5     at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.checkTerminated(ObservableObserveOn.java:281)
 6     at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.drainNormal(ObservableObserveOn.java:172)
 7     at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.run(ObservableObserveOn.java:255)
 8     at io.reactivex.android.schedulers.HandlerScheduler$ScheduledRunnable.run(HandlerScheduler.java:124)
 9     at android.os.Handler.handleCallback(Handler.java:883)
10     at android.os.Handler.dispatchMessage(Handler.java:100)
11     at android.os.Looper.loop(Looper.java:214)
12     at android.app.ActivityThread.main(ActivityThread.java:7682)
13     at java.lang.reflect.Method.invoke(Native Method)
14     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:516)
15     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
16 Caused by: java.lang.NullPointerException: Callable returned null
17     at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39)
18     at io.reactivex.internal.operators.observable.ObservableFromCallable.subscribeActual(ObservableFromCallable.java:43)
19     at io.reactivex.Observable.subscribe(Observable.java:12267)
20     at io.reactivex.internal.operators.observable.ObservableSubscribeOn$SubscribeTask.run(ObservableSubscribeOn.java:96)
21     at io.reactivex.Scheduler$DisposeTask.run(Scheduler.java:578)
22     at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
23     at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
24     at java.util.concurrent.FutureTask.run(FutureTask.java:266)
25     at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
26     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
27     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
28     at java.lang.Thread.run(Thread.java:919)

It can be seen that in the above Error stack information, it does not give the call path of this Error , in the actual project. It can be seen that the stack with errors provides less effective information. We can only know that it is due to label Call() returned Null here, causing an Error. But we can't judge where the callable was created. At this time, we can only judge the approximate location of the current previous code in combination with the log context, and then check it step by step.

 1        public final class ObservableFromCallable<T> extends Observable<T> implements Callable<T> {
 2
 3
 4        @Override
 5        public void subscribeActual(Observer<? super T> observer) {
 6        DeferredScalarDisposable<T> d = new DeferredScalarDisposable<T>(observer);
 7        observer.onSubscribe(d);
 8        if (d.isDisposed()) {
 9            return;
10        }
11        T value;
12        try {
13            // callable.call() returns Null here and passes it to the errorHandler of RxJavaPlugins
14            value = ObjectHelper.requireNonNull(callable.call(), "Callable returned null");
15        } catch (Throwable e) {
16            Exceptions.throwIfFatal(e);
17            if (!d.isDisposed()) {
18                observer.onError(e);
19            } else {
20                RxJavaPlugins.onError(e);
21            }
22            return;
23        }
24        d.complete(value);
25       }
26
27      }

A meal is as fierce as a tiger. There are many operations. Combined with some context logs, we find that null is returned here, resulting in an error

1    backgroundTask(Callable<Any> {
2    Log.i(TAG, "btn_rx_task: ")
3    Thread.sleep(30)
4    return@Callable null
5    })?.subscribe()

1/**
2 * Create an rx subthread task Observable
3 */
4    private fun <T> backgroundTask(callable: Callable<T>?): Observable<T>? {
5    return Observable.fromCallable(callable)
6            .compose(IOMain())
7    }

If there are many callable cases, check callable one by one at this time, and it is estimated that you will spit blood.

Is there any better way, such as monitoring? Complete print stack information.

The first solution is to customize the Hook solution

First, let's think about what a stack is?

In my understanding, the stack is used to store information about the current execution of our program. In Java, we use Java Lang. thread #getstacktrace # can get the stack information of the current thread. Note that it is the stack of the current thread.

The exception thrown by RxJava is when the Callable#call method is executed. Naturally, it prints the method call stack of #Callable#call. If the calling thread of Callable#call is inconsistent with the creating thread of callable, it will not get the stack when creating callable.

In fact, what we need to know is the place where callable is created, which corresponds to the place where our project reports errors. Naturally, it is "observable" The call stack of the fromcallable method.

At this time, we can use Hook to Hook our code

For convenience, we have adopted wenshu great God's Hook framework, github. If you want to go to the Hook manually, you can take a look at the article I wrote two years ago, which introduces some common Hook methods.

Soon, we wrote the following code to hook the #Observable#fromCallable # method

 1    fun hookRxFromCallable() {
 2//        DexposedBridge.findAndHookMethod(ObservableFromCallable::class.java, "subscribeActual", Observer::class.java, RxMethodHook())
 3        DexposedBridge.findAndHookMethod(
 4            Observable::class.java,
 5            "fromCallable",
 6            Callable::class.java,
 7            object : XC_MethodHook() {
 8                override fun beforeHookedMethod(param: MethodHookParam?) {
 9                    super.beforeHookedMethod(param)
10                    val args = param?.args
11                    args ?: return
12
13                    val callable = args[0] as Callable<*>
14                    args[0] = MyCallable(callable = callable)
15
16                }
17
18                override fun afterHookedMethod(param: MethodHookParam?) {
19                    super.afterHookedMethod(param)
20                }
21            })
22    }
23
24    class MyCallable(private val callable: Callable<*>) : Callable<Any> {
25
26        private val TAG = "RxJavaHookActivity"
27        val buildStackTrace: String?
28
29        init {
30            buildStackTrace = Rx2Utils.buildStackTrace()
31        }
32
33        override fun call(): Any {
34            Log.i(TAG, "call: ")
35            val call = callable.call()
36            if (call == null) {
37                Log.e(TAG, "call should not return null: buildStackTrace is $buildStackTrace")
38            }
39            return call
40        }
41
42    }

Execute our code again

1    backgroundTask(Callable<Any> {
2    Log.i(TAG, "btn_rx_task: ")
3    Thread.sleep(30)
4    return@Callable null
5    })?.subscribe()

We can see that when our Callable returns empty, the error message will contain the code of our project, perfect.

RxJavaExtensions

Recently, we found this framework on Github, which can also help us solve the problem of incomplete information in RxJava exception process. Its basic usage is as follows:

use

https://github.com/akarnokd/R...

The first step is to introduce the dependency library

1dependencies {
2    implementation "com.github.akarnokd:rxjava2-extensions:0.20.10"
3}

Step 2: enable error tracking first:

1RxJavaAssemblyTracking.enable();

Step 3: print the stack when the exception is thrown

 1    /**
 2     * Set the global onErrorHandler.
 3     */
 4    fun setRxOnErrorHandler() {
 5        RxJavaPlugins.setErrorHandler { throwable: Throwable ->
 6            val assembled = RxJavaAssemblyException.find(throwable)
 7            if (assembled != null) {
 8                Log.e(TAG, assembled.stacktrace())
 9        }
10            throwable.printStackTrace()
11            Log.e(TAG, "setRxOnErrorHandler: throwable is $throwable")
12        }
13        }

principle

RxJavaAssemblyTracking.enable();

 1public static void enable() {
 2    if (lock.compareAndSet(false, true)) {
 3
 4        // Several methods are omitted
 5
 6        RxJavaPlugins.setOnObservableAssembly(new Function<Observable, Observable>() {
 7            @Override
 8            public Observable apply(Observable f) throws Exception {
 9                if (f instanceof Callable) {
10                    if (f instanceof ScalarCallable) {
11                        return new ObservableOnAssemblyScalarCallable(f);
12                    }
13                    return new ObservableOnAssemblyCallable(f);
14                }
15                return new ObservableOnAssembly(f);
16            }
17        });
18
19
20        lock.set(false);
21    }
22    }

As you can see, it calls rxjavaplugins Setonobserveassembly , method, which sets the , rxjavaplugins onobserveassembly , variable

The Observable#fromCallable method mentioned above will call rxjavaplugins Onassembly method. When our onobserveassembly is not null, we will call the apply method for conversion.

 1     public static <T> Observable<T> fromCallable(Callable<? extends T> supplier) {
 2    ObjectHelper.requireNonNull(supplier, "supplier is null");
 3    return RxJavaPlugins.onAssembly(new ObservableFromCallable<T>(supplier));
 4    }
 5    public static <T> Observable<T> onAssembly(@NonNull Observable<T> source) {
 6    Function<? super Observable, ? extends Observable> f = onObservableAssembly;
 7    if (f != null) {
 8        return apply(f, source);
 9    }
10    return source;
11    }

Therefore, when we set rxjavaassemblytracking Enable(), the supplier passed in by Observable#fromCallable will eventually be wrapped in a layer, which may be ObservableOnAssemblyScalarCallable, ObservableOnAssemblyCallable, ObservableOnAssembly. For typical decorator mode applications, it has to be said here that this point provided by RxJava is cleverly designed, which is very convenient for us to make some hook s.

Let's take the observable on assembly callable as an example

 1   final class ObservableOnAssemblyCallable<T> extends Observable<T> implements Callable<T> {
 2
 3    final ObservableSource<T> source;
 4
 5    // Save the stack information of Callable where it was created
 6    final RxJavaAssemblyException assembled;
 7
 8    ObservableOnAssemblyCallable(ObservableSource<T> source) {
 9        this.source = source;
10        this.assembled = new RxJavaAssemblyException();
11    }
12
13    @Override
14    protected void subscribeActual(Observer<? super T> observer) {
15        source.subscribe(new OnAssemblyObserver<T>(observer, assembled));
16    }
17
18    @SuppressWarnings("unchecked")
19    @Override
20    public T call() throws Exception {
21        try {
22            return ((Callable<T>)source).call();
23        } catch (Exception ex) {
24            Exceptions.throwIfFatal(ex);
25            throw (Exception)assembled.appendLast(ex);
26    }
27    }
28    }
29
30   public final class RxJavaAssemblyException extends RuntimeException {
31
32    private static final long serialVersionUID = -6757520270386306081L;
33
34    final String stacktrace;
35
36    public RxJavaAssemblyException() {
37        this.stacktrace = buildStackTrace();
38    }
39    }

It can be seen that he directly saves the stack information of the Callable when constructing the method of ObservableOnAssemblyCallable. The class is RxJavaAssemblyException.

When error is reported, RxJavaAssemblyException is called Find (throwable) method to judge whether it is RxJavaAssemblyException. If so, return it directly.

 1    public static RxJavaAssemblyException find(Throwable ex) {
 2    Set<Throwable> memory = new HashSet<Throwable>();
 3    while (ex != null) {
 4        if (ex instanceof RxJavaAssemblyException) {
 5            return (RxJavaAssemblyException)ex;
 6        }
 7
 8        if (memory.add(ex)) {
 9            ex = ex.getCause();
10        } else {
11            return null;
12    }
13    }
14    return null;
15    }

So far, RxJavaAssemblyTracking has made it clear that the process of printing out the error information completely is to use a wrapper class when creating a Callable, report the error information in the constructor, and replace the error information with the saved error information when an error occurs.

Our custom Hook also uses this idea to expose the stack created by callable in advance.

Some thinking

We usually don't bring the above scheme online. Why? Because for each callable, we need to save the stack in advance, and obtaining the stack is time-consuming. Is there any way?

If the project has access to the Matrix, we can consider borrowing the idea of Matrix trace, because #i# and #o# AppMethodBeat#o# are inserted before and after the method, so that when we execute the method, because of the insertion pile, we can easily obtain the time-consuming of method execution and the call stack of the method.

1// Step 1: you need to create beginRecord in the right place
2AppMethodBeat.IndexRecord  beginRecord = AppMethodBeat.getInstance().maskIndex("AnrTracer#dispatchBegin");
3// Step 2: the call stack information of the method is in data
4 long[] data = AppMethodBeat.getInstance().copyData(beginRecord);
5 Step 3:
6 take data Into what we want stack(After a preliminary look at the code, we need to modify it trace (code of)

github:https://github.com/gdutxiaoxu...

reference material
rxjava-2-doesnt-tell-the-error-line
how-to-log-a-stacktrace-of-all-exceptions-of-rxjava2

last

Don't forget to like your friends and pay more attention to your collection~

Topics: Android