主頁 > 移動端開發 > RxJava 例外時堆疊顯示不正確?解決方法都在這里

RxJava 例外時堆疊顯示不正確?解決方法都在這里

2023-02-21 09:17:23 移動端開發

本文首發我的博客,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,

image-20211122164509577

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")
        }
    }

image-20211122170335525

原理

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#iAppMethodBeat#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

標籤:其他

上一篇:【FAQ】集成分析服務的常見問題及解決方案

下一篇:防抖與節流:教你傾聽時插話的技巧

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more