RxJava 異常時堆棧顯示不正確?解決方法都在這里

本文首發(fā)我的博客,github 地址
大家好,我是徐公,今天為大家?guī)淼氖?RxJava 的一個血案,一行代碼 return null 引發(fā)的。
前陣子,組內(nèi)的同事反饋說 RxJava 在 debug 包 crash 了,捕獲到的異常信息不全。(即我們捕獲到的堆棧沒有包含我們自己代碼,都是一些系統(tǒng)或者 RxJava 框架的代碼)
典型的一些 error 信息如下:

可以看到,上面的 Error 堆棧信息中,它并沒有給出這個 Error 在實(shí)際項(xiàng)目中的調(diào)用路徑。可以看到,報(bào)錯的堆棧,提供的有效信息較少, 我們只能知道是由于 callable.call() ?這里返回了 Null,導(dǎo)致出錯。卻不能判斷 callable 是哪里創(chuàng)建的,這時候我們只能結(jié)合日志上下文,判斷當(dāng)前之前的代碼大概在哪里,再逐步排查。
public?final?class?ObservableFromCallable<T>?extends?Observable<T>?implements?Callable<T>?{
????
????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);
????}
}
一頓操作猛如虎,很多,我們結(jié)合一些讓下文日志,發(fā)現(xiàn)是這里返回了 null,導(dǎo)致出錯
backgroundTask(Callable<Any>?{
????Log.i(TAG,?"btn_rx_task:?")
????Thread.sleep(30)
????return@Callable?null
})?.subscribe()/**
?*?創(chuàng)建一個rx的子線程任務(wù)Observable
?*/
private?fun?<T>?backgroundTask(callable:?Callable<T>?):?Observable<T>??{
????return?Observable.fromCallable(callable)
????????????.compose(IOMain())
}
如果遇到 callable 比較多的情況下,這時候 一個個排查 callable,估計(jì)搞到你吐血。
那有沒有什么較好的方法,比如做一些監(jiān)控?完整打印堆棧信息。
第一種方案,自定義 Hook 解決
首先,我們先來想一下,什么是堆棧?
在我的理解里面,堆棧是用來儲存我們程序當(dāng)前執(zhí)行的信息。在 Java 當(dāng)中,我們通過 java.lang.Thread#getStackTrace
可以拿到當(dāng)前線程的堆棧信息,注意是當(dāng)前線程的堆棧。
而 RxJava 拋出異常的地方,是在執(zhí)行 Callable#call ?方法中,它打印的自然是 Callable#call
的方法調(diào)用棧,而如果 Callable#call 的調(diào)用線程跟 callable 的創(chuàng)建線程不一致,那肯定拿不到 創(chuàng)建 callable 時候的堆棧。
而我們實(shí)際上需要知道的是 callable 創(chuàng)建的地方,對應(yīng)到我們我們項(xiàng)目報(bào)錯的地方,那自然是 Observable.fromCallable
方法的調(diào)用棧。
這時候,我們可以采用 Hook 的方式,來 Hook 我們的代碼
為了方便,我們這里采用了 wenshu 大神的 Hook 框架, github, 想自己手動去 Hook 的,可以看一下我兩年前寫的文章 Android Hook 機(jī)制之簡單實(shí)戰(zhàn),里面有介紹介紹一些常用的 Hook 手段。
很快,我們寫出了如下代碼,對 Observable#fromCallable
方法進(jìn)行 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
????????}
????}
再次執(zhí)行我們的代碼
backgroundTask(Callable<Any>?{
????Log.i(TAG,?"btn_rx_task:?")
????Thread.sleep(30)
????return@Callable?null
})?.subscribe()
可以看到,當(dāng)我們的 Callable 返回為 empty 的時候,這時候報(bào)錯的信息會含有我們項(xiàng)目的代碼, perfect。

RxJavaExtensions
最近,在 Github 上面發(fā)現(xiàn)了這一個框架,它也可以幫助我們解決 RxJava 異常過程中信息不全的問題。它的基本使用如下:
使用
https://github.com/akarnokd/RxJavaExtensions
第一步,引入依賴庫
dependencies?{
????implementation?"com.github.akarnokd:rxjava2-extensions:0.20.10"
}
第二步:先啟用錯誤追蹤:
RxJavaAssemblyTracking.enable();
第三步:在拋出異常的異常,打印堆棧
????/**
?????*?設(shè)置全局的?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>()?{
????????????
????????????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);
????}
}
可以看到,它調(diào)用了 RxJavaPlugins.setOnObservableAssembly
方法,設(shè)置了 RxJavaPlugins onObservableAssembly
變量
而我們上面提到的 Observable#fromCallable 方法,它里面會調(diào)用 RxJavaPlugins.onAssembly 方法,當(dāng)我們的 onObservableAssembly 不為 null 的時候,會調(diào)用 apply 方法進(jìn)行轉(zhuǎn)換。
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;
}
因此,即當(dāng)我們設(shè)置了 RxJavaAssemblyTracking.enable()
, Observable#fromCallable
傳遞進(jìn)來的 supplier,最終會包裹一層,可能是 ObservableOnAssemblyScalarCallable,ObservableOnAssemblyCallable,ObservableOnAssembly。典型的裝飾者模式應(yīng)用,這里不得不說,RxJava 對外提供的這個點(diǎn),設(shè)計(jì)得真巧妙,可以很方便我們做一些 ?hook。
我們就以 ObservableOnAssemblyCallable 看一下
final?class?ObservableOnAssemblyCallable<T>?extends?Observable<T>?implements?Callable<T>?{
????final?ObservableSource<T>?source;
????//?將在哪里創(chuàng)建的?Callable?的堆棧信息保存下來
????final?RxJavaAssemblyException?assembled;
????ObservableOnAssemblyCallable(ObservableSource<T>?source)?{
????????this.source?=?source;
????????this.assembled?=?new?RxJavaAssemblyException();
????}
????
????protected?void?subscribeActual(Observer<??super?T>?observer)?{
????????source.subscribe(new?OnAssemblyObserver<T>(observer,?assembled));
????}
???? ("unchecked")
????
????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 的構(gòu)造方法的時候,直接將 Callable 的堆棧信息保存下來,類為 RxJavaAssemblyException。
而當(dāng) error 報(bào)錯的時候,調(diào)用 ?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 信息完整打印出來的流程已經(jīng)講明白了,其實(shí)就是在創(chuàng)建 Callable 的時候,采用一個包裝類,在構(gòu)造函數(shù)的時候,將 error 信息報(bào)錯下來,等到出錯的時候,再將 error 信息,替換成保存下來的 error信息。
我們的自定義 Hook 也是利用這種思路,提前將 callable 創(chuàng)建的堆棧暴露下來,換湯不換藥。
一些思考
上述的方案我們一般不會帶到線上,為什么呢? 因?yàn)閷τ诿恳粋€ callable,我們需要提前保存堆棧,而獲取堆棧是耗時的。那有沒有什么方法呢?
如果項(xiàng)目有接入 Matrix 的話,可以考慮借用 Matrix trace 的思想,因?yàn)樵诜椒ㄇ昂蟛迦?AppMethodBeat#i
和 AppMethodBeat#o
這樣當(dāng)我們執(zhí)行方法的時候,因?yàn)椴鍢读?,我們可以方便得獲取到方法執(zhí)行耗時,以及方法的調(diào)用棧。
//?第一步:需要在合適的實(shí)際先生成?beginRecord
AppMethodBeat.IndexRecord??beginRecord?=?AppMethodBeat.getInstance().maskIndex("AnrTracer#dispatchBegin");
//?第二步:方法的調(diào)用棧信息在?data?里面
long[]?data?=?AppMethodBeat.getInstance().copyData(beginRecord);
第三步:
將?data?轉(zhuǎn)化為我們想要的?stack(初步看了代碼,需要我們修改?trace?的代碼)