本文首發我的博客,github 地址 大家好,我是徐公,今天為大家帶來的是 RxJava 的一個血案,一行代碼 return null 引發的。 前陣子,組內的同事反饋說 RxJava 在 debug 包 crash 了,捕獲到的異常信息不全。(即我們捕獲到的堆棧沒有包含我們自己代碼,都是一些系統或 ...
本文首發我的博客,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。
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")
}
}
原理
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 = AppMethodBeat.getInstance().copyData(beginRecord);
第三步:
將 data 轉化為我們想要的 stack(初步看了代碼,需要我們修改 trace 的代碼)
參考資料
rxjava-2-doesnt-tell-the-error-line
how-to-log-a-stacktrace-of-all-exceptions-of-rxjava2
推薦閱讀
騰訊 Matrix 增量編譯 bug 解決之路,PR 已通過
我是站在巨人的肩膀上成長起來的,同樣,我也希望成為你們的巨人。覺得不錯的話可以關註一下我的微信公眾號徐公。