本文首發我的博客,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 = https://www.cnblogs.com/gdutxiaoxu/archive/2023/02/20/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,

RxJavaExtensions
最近,在 Github 上面發現了這一個框架,它也可以幫助我們解決 RxJava 例外程序中資訊不全的問題,它的基本使用如下:
使用
https://github.com/akarnokd/RxJavaExtensions
第一步,引入依賴庫
dependencies {
implementation "com.github.akarnokd:rxjava2-extensions:0.20.10"
}
第二步:先啟用錯誤追蹤:
RxJavaAssemblyTracking.enable();
第三步:在拋出例外的例外,列印堆疊
/**
* 設定全域的 one rrorHandler,
*/
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>() {
@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#i 和 AppMethodBeat#o 這樣當我們執行方法的時候,因為插樁了,我們可以方便得獲取到方法執行耗時,以及方法的呼叫堆疊,
// 第一步:需要在合適的實際先生成 beginRecord
AppMethodBeat.IndexRecord beginRecord = AppMethodBeat.getInstance().maskIndex("AnrTracer#dispatchBegin");
// 第二步:方法的呼叫堆疊資訊在 data 里面
long[] data = https://www.cnblogs.com/gdutxiaoxu/archive/2023/02/20/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.uj5u.com/yidong/544532.html
標籤:其他
下一篇:防抖與節流:教你傾聽時插話的技巧
