The phenomenon of

Hello everyone, I am Xu, today to bring you is a blood case RxJava, a line of code return NULL triggered.

Some time ago, some colleagues in the group reported that the RxJava debug package crashed, and the captured exception information was not complete. (I.e., the stack we captured does not contain our own code, but some system or RxJava framework code)

Typical error messages are as follows:

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.jav a:281) at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.drainNormal(ObservableObserveOn.java:17 2) 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)Copy the code

As you can see from the Error stack information above, it does not give the path where this Error will be called in the actual project. As you can see, the error stack provides less useful information than callable.call(), which returns Null, causing the error. However, we cannot determine where the callable was created. At this time, we can only determine where the current code is based on the log context, and then step by step.

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() returns Null, And passed to the 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); }}Copy the code

A number of operations like a tiger, we combine some of the following log, found that null is returned here, causing an error

backgroundTask(Callable<Any> {
    Log.i(TAG, "btn_rx_task: ")
    Thread.sleep(30)
    return@Callable null})? .subscribe()Copy the code
/** * Create an Rx subthread task Observable */
private fun <T> backgroundTask(callable: Callable<T>?: Observable<T>? {
    return Observable.fromCallable(callable)
            .compose(IOMain())
}
Copy the code

If you encounter more callable cases, this time one by one check the callable, it is estimated that you vomit blood.

Is there a better way, like some surveillance? Prints the complete stack information.

The first solution is customized Hook solution

First of all, let’s think about, what is a stack?

In my understanding, the stack is used to store information about the current execution of our program. In Java, java.lang.Thread#getStackTrace is used to retrieve the stack of the current thread.

Where RxJava throws an exception is in the execution of the Callable# Call method, which naturally prints the call stack of the Callable# Call method, and if the calling thread of the Callable# Call is different from the creation thread of the callable # Call, You can’t get the stack that created the callable.

What we really need to know is where the callable is created, and that corresponds to where our project reported an error, which is of course the call stack for the Observable.fromCallable method.

At this point, we can Hook our code in a Hook way

For convenience, we here use the Hook framework of Wenshu God, Github, want to Hook manually, you can look at the article I wrote two years ago Android Hook mechanism simple combat, there is an introduction to introduce some common Hook means.

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

    fun hookRxFromCallable(a) {
// 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)
                    valargs = 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(a): 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
        }

    }

Copy the code

Execute our code again

backgroundTask(Callable<Any> {
    Log.i(TAG, "btn_rx_task: ")
    Thread.sleep(30)
    return@Callable null})? .subscribe()Copy the code

It can be seen that when our Callable is returned as empty, the error message will contain the code of our project, perfect.

The second option, RxJavaExtensions

Recently, a framework was found on Github, which can also help us solve the problem of incomplete information during RxJava exceptions. Its basic use is as follows:

use

Github.com/akarnokd/Rx…

The first step is to introduce the dependency library

Dependencies {implementation "com. Making. Akarnokd: rxjava2 - extensions: 0.20.10"}Copy the code

Step 2: Enable error tracking first:

RxJavaAssemblyTracking.enable();
Copy the code

Step 3: In the exception that throws the exception, print the stack

/** * Set the global 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") } }Copy the code

The principle of

RxJavaAssemblyTracking.enable();

public static void enable() { if (lock.compareAndSet(false, True)) {/ / omitted several methods RxJavaPlugins setOnObservableAssembly (new Function < observables, 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); }}Copy the code

. As you can see, it calls the RxJavaPlugins setOnObservableAssembly method, set up RxJavaPlugins onObservableAssembly variables

The Observable#fromCallable method we mentioned above calls the rxJavaplugins.onAssembly method. When our onObservableAssembly is not null, The Apply method is called for the transformation.

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;
}
Copy the code

So that when we set the RxJavaAssemblyTracking. The enable (), Observable# fromCallable passed: supplier, will eventually wrapped a layer, Probably be ObservableOnAssemblyScalarCallable ObservableOnAssemblyCallable ObservableOnAssembly. Typical decorator mode application, here I have to say, RxJava provides this point, really clever design, can be very convenient for us to do some hooks.

We have to ObservableOnAssemblyCallable look

final class ObservableOnAssemblyCallable<T> extends Observable<T> implements Callable<T> { final ObservableSource<T> source; // Save the stack information of the Callable created in final RxJavaAssemblyException; 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(); }}Copy the code

As you can see, he is directly in the construction of the ObservableOnAssemblyCallable method, directly to the Callable stack information preserved, such as RxJavaAssemblyException.

And when the error error, call RxJavaAssemblyException. Find (throwable), determine whether RxJavaAssemblyException, so, return directly.

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; }Copy the code

RxJavaAssemblyTracking uses a wrapper class to generate an error message when creating a Callable. When an error message is generated, an error message is reported with the RxJavaAssemblyTracking file. Replace the error message with the saved error message.

Our custom hooks take advantage of this idea by exposing the stack created by Callable ahead of time.

Some think

We don’t usually carry these solutions online. Why? Because for each callable, we need to save the stack ahead of time, and fetching the stack is time consuming. Is there any way to do that?

If your project has access to a Matrix, you might want to borrow the idea of Matrix trace, because AppMethodBeat# I and AppMethodBeat#o are inserted before and after the method so that when we execute the method, because it’s staked, We can easily obtain method execution time, as well as method call stack.

// Step 1: Mr Requires actual into beginRecord AppMethodBeat. IndexRecord beginRecord = AppMethodBeat.getInstance().maskIndex("AnrTracer#dispatchBegin"); The second step: / / method call stack information in data long [] data = AppMethodBeat. GetInstance (). CopyData (beginRecord); Step 3: Convert data to the stack we want (preliminary look at the code, we need to modify the trace code)Copy the code

The resources

rxjava-2-doesnt-tell-the-error-line

how-to-log-a-stacktrace-of-all-exceptions-of-rxjava2

Recommended reading

My 5 years of Android learning road, those years together stepped on the pit

Tencent Matrix incremental compilation bug solution, PR has been passed