Error!一個沒法重現的錯誤怎麼去Debug?

2018 年 10 月 10 日的這天,咱們的團隊發佈了一個新版本的 React Native 應用程序。咱們很高興又爲咱們的用戶交付了新功能。java

可是,恐怖的事情發生了!node

發佈幾個小時後,咱們忽然收到不少 Android 崩潰事件。react

Android 版本上發生了 10000 次崩潰android

咱們的崩潰報告工具Sentry像着火了同樣!面試

全部的新錯誤都是相似「JSApplicationIllegalArgumentException Error while updating property ‘left’ in shadow node of type: RCTView」這樣的。react-native

在 React Native 中,若是你使用錯誤的類型設置屬性,一般會發生這種狀況。可是,爲何咱們在測試應用程序時沒有發現這個錯誤?咱們的新版本已經在多個設備上測試過了。數組

此外,錯誤彷佛是隨機的,彷佛在遇到屬性和陰影節點類型的組合時會發生這個錯誤。如下是其中的 3 個錯誤:安全

根據 Sentry 的報告,這些錯誤彷佛在任意設備和任意 Android 版本上都會發生。數據結構

大多數Android 8.0.0崩潰但這與咱們的用戶羣一致

大多數Android 8.0.0崩潰但這與咱們的用戶羣一致多線程

 

大多數Android 8.0.0崩潰但這與咱們的用戶羣一致

讓咱們來重現錯誤

修復錯誤的第一步是重現錯誤。所幸的是,由於有 Sentry 日誌,咱們知道用戶在觸發崩潰以前正在作什麼。

絕大多數的崩潰都是發生在用戶打開應用程序的時候。

如今咱們也嘗試重現一下。咱們在 6 臺不一樣的 Android 設備上安裝從應用商店下載的 App,惋惜的是,並無發生崩潰!並且,在開發模式下就更不可能在本地重現這個錯誤了。

看來這樣作彷佛毫無心義。不管如何,崩潰彷佛是隨機發生的。發生崩潰的機率約爲 10%,也就是說,基本上啓動 App10 次會有一次發生崩潰。

分析堆棧跟蹤信息

爲了可以重現崩潰,咱們試着去了解問題出在哪裏。

 

好吧,如前所述,咱們遇到了幾個不同的錯誤。它們都有相似但不徹底相同的堆棧跟蹤信息。

咱們先來分析第一個:

java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
    at android.support.v4.util.Pools$SimplePool.release(Pools.java:116)
    at com.facebook.react.bridge.DynamicFromMap.recycle(DynamicFromMap.java:40)
    at com.facebook.react.uimanager.LayoutShadowNode.setHeight(LayoutShadowNode.java:168)
    at java.lang.reflect.Method.invoke(Method.java)
    ...

java.lang.reflect.InvocationTargetException: null
    at java.lang.reflect.Method.invoke(Method.java)
    ...

com.facebook.react.bridge.JSApplicationIllegalArgumentException: Error while updating property 'height' in shadow node of type: RNSVGSvgView
    at com.facebook.react.uimanager.ViewManagersPropertyCache$PropSetter.updateShadowNodeProp(ViewManagersPropertyCache.java:113)
    ...

咱們找到了發生錯誤的地方:android/support/v4/util/Pools.java。

咱們已經很是深刻到 Android 支持庫,但不肯定如今能夠從中推斷出多少信息。

使用另外一種方式

另外一種方法是檢查咱們在新版本代碼中所作的修改,特別是那些會影響原生 Android 代碼的修改。咱們發現了 2 個可能性:

  • 咱們升級了 Native Navigation,這是一種在 Android 上爲每一個屏幕使用原生片斷的導航解決方案;

  • 咱們升級了 react-native-svg。有一些與 SVG 組件相關的異常,但有些與它沒有關係,因此很難說。

由於沒法重現錯誤,咱們最好的選擇是:

  • 回退 2 個庫中的一個;

  • 只發布給 10%的用戶;

  • 與這些用戶確認,看看新版本有沒有發生崩潰。這樣就能夠驗證咱們的假設。

要回退哪一個庫呢?

一種辦法是經過拋硬幣來決定,但咱們真的要這麼作嗎?

深究這一點

好吧,讓咱們深刻挖掘以前的堆棧跟蹤信息,看看是否能夠肯定選擇回退哪一個庫。

/**
 * Simple (non-synchronized) pool of objects.
 *
 * @param  The pooled type.
 */
public static class SimplePool implements Pool {
    private final Object[] mPool;

    private int mPoolSize;

    ...

    @Override
    public boolean release(T instance) {
        if (isInPool(instance)) {
            throw new IllegalStateException("Already in the pool!");
        }
        if (mPoolSize < mPool.length) {
            mPool[mPoolSize] = instance;
            mPoolSize++;
            return true;
        }
        return false;
    }

以上是崩潰發生的地方。錯誤是java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1,意思是說,mPool 是一個大小爲 10 的數組,但 mPoolSize = -1。

除了上面的 recycle 方法以外,能夠修改 mPoolSize 的另外一個地方是 SimplePool 類的 acquire 方法:

public T acquire() {
    if (mPoolSize > 0) {
        final int lastPooledIndex = mPoolSize - 1;
        T instance = (T) mPool[lastPooledIndex];
        mPool[lastPooledIndex] = null;
        mPoolSize--;
        return instance;
    }
    return null;
}

所以,致使 mPoolSize 變爲 -1 的惟一多是在 mPoolSize=0 時繼續執行 mPoolSize–。 但在 mPoolSize > 0 時,這種狀況怎麼可能會發生呢?

咱們在 Android Studio 中設置了一個斷點,並檢查啓動應用程序時發生了什麼。個人意思是,由於有一個 if 條件,這段代碼不該該會出現故障!

最後,啓示!

DynamicFromMap有一個靜態引用SimplePool。

private static final Pools.SimplePool<DynamicFromMap> sPool = new Pools.SimplePool<>(10);

若是對軟件測試、接口測試、自動化測試、性能測試、LR腳本開發、面試經驗交流。感興趣能夠175317069,羣內會有不按期的發放免費的資料連接,這些資料都是從各個技術網站蒐集、整理出來的,若是你有好的學習資料能夠私聊發我,我會註明出處以後分享給你們。

在幾十次點擊播放按鈕後,咱們能夠經過精心放置的斷點查看SimplePool.acquire並經過React Native SimplePool.release調用mqt_native_modules線程來管理React組件的樣式屬性(在組件下面width)

但同時也被主線程調用!

從上面咱們能夠看到,它被用於更新主線程上的 fill prop,這個屬性一般屬於 react-native-svg 組件!實際上,react-native-svg 只在版本 7 以後纔開始使用 DynamicFromMap 來提升原生 svg 動畫的性能。

函數實際上被 2 個線程調用,但 DynamicFromMap 沒有以線程安全的方式使用 SimplePool。「線程安全」又是什麼鬼?

線程安全理論

由於 JavaScript 是單線程的,所以 JavaScript 開發人員一般不須要處理線程安全問題。

另外一方面,Java 支持併發或多線程概念。多個線程能夠在單個程序中運行,而且可能會併發訪問公共數據結構,可能會致使意外的結果。

讓咱們舉一個簡單的例子,在下圖中,線程 A 和線程 B 都:

  • 將整數讀入內存;

  • 增長它的價值;

  • 將它返回。

在線程 A 完成更新以前,線程 B 可能會訪問數據的值。咱們指望它們是兩個單獨的遞增值操做,最終結果爲 19,但結果可能會是 18。對於這樣狀況,數據的最終狀態取決於線程操做的順序,稱爲競態條件。競態條件的問題在於它們不必定老是會發生。對於上述的狀況,線程 B 在遞增值以前還有更多的工做要作,爲線程 A 提供足夠的時間來更新值。這就解釋了重現崩潰的隨機性和不可能性。

若是操做能夠由不少線程同時完成,則數據結構被認爲是線程安全的,就不會有出現競態條件的風險。

當一個線程讀取一個特定數據元素時,不該該讓其餘線程修改或刪除這個元素(這稱爲原子性)。在咱們以前的示例中,若是更新週期是原子的,就能夠避免出現競態條件。線程 B 將等待線程 A 完成操做。

在咱們的例子中,這是可能發生的事情:

因爲 DynamicFromMap 持有對 SimplePool 的靜態引用,所以不一樣線程的多個 DynamicFromMap 調用致使能夠同時調用 SimplePool 的 acquire 方法。

在上圖中,線程 A 調用 acquire 方法,得出條件爲 true,但還沒有減少 mPoolSize 的值(與線程 B 共享),而線程 B 同時調用該方法,並得出相同的條件。而後每一個單獨的調用都將減小 mPoolSize 的值,這就是爲何你會得到一個錯誤的值。

修復錯誤

咱們在 react-native 上發現了一個未合併的 PR,這個 PR 修復了線程安全問題。

而後,咱們部署了一個修補版本的 react native,將其發佈給咱們的用戶。崩潰問題終於獲得瞭解決!

這個修復將包含在 React Native 的下一個小版本 0.57 中。

爲了修復這個錯誤,咱們確實作出了很大的努力,但這也是一個深刻了解 react-native 和 react-native-svg 的絕佳機會。一個好的調試器和一些很好的斷點很長的路要走。但願你也學到了一些有用的東西!

相關文章
相關標籤/搜索