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崩潰但這與咱們的用戶羣一致
修復錯誤的第一步是重現錯誤。所幸的是,由於有 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 的絕佳機會。一個好的調試器和一些很好的斷點很長的路要走。但願你也學到了一些有用的東西!