调试无法播放的错误

2018年10月10日,我们的团队在React Native上发布了该应用程序的新版本。 我们为此感到高兴和自豪。

但是令人恐惧的是:几个小时后,Android的故障数量突然增加。


10,000次Android崩溃

我们的Sentry崩溃监控工具正在疯狂。

在所有情况下,我们JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView"看到类似JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView"

在React Native中,如果您使用错误的类型设置属性,通常会发生这种情况。 但是为什么在测试过程中没有出现错误? 在我们这里,每个开发人员都会在多个设备上仔细测试新版本。

错误也似乎是随机的,它们似乎是在属性和类型阴影节点的任何组合上产生的。 例如,这是前三个:

  • Error while updating property 'paddingTop' in shadow node of type: RCTView
  • Error while updating property 'height' in shadow node of type: RCTImageView
  • Error while updating property 'fill' of a view managed by: RNSVGPath

根据Sentry报告判断,该错误似乎发生在任何设备和任何版本的Android上。


Android 8.0.0崩溃最多,但是这与我们的用户群一致

让我们播放吧!


因此,修复错误之前的第一步是重现它,对吗? 幸运的是,多亏了Sentry日志,我们才能找出用户在崩溃之前所做的事情。

Ta-a-ak,让我们看看...



嗯,在大多数情况下,用户只需打开应用程序,并且-繁荣,发生崩溃。

好的,让我们再试一次。 我们将应用程序安装在六个Android设备上,将其打开并退出几次。 没有毛刺! 而且,不可能在开发模式下本地播放。

好吧,这似乎毫无意义。 故障仍然是非常随机的,并且在10%的情况下发生。 看起来应用程序在启动时崩溃的可能性为十分之一。

堆栈跟踪分析


要重现此失败,让我们尝试了解它的来源...


如前所述,我们有几个不同的错误。 每个人都有相似但略有不同的痕迹。

好吧,让我们来第一个:

 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代码的代码。 产生两个假设:

  • 我们更新了本机导航 ,其中每个屏幕都使用Android的本机片段。
  • 我们更新了react-native-svg 。 与SVG组件有关的例外情况很少,但事实并非如此。

我们目前无法重现错误,因此最佳策略是:

  1. 回滚这两个库中的一个,将其分发给10%的用户,这在Play商店中很容易完成,请与几个用户核实故障是否仍然存在。 因此,我们证实或驳斥了这一假设。


    但是如何选择要回滚的库? 当然,您可以扔硬币,但这是最佳选择吗?


    切入点


    让我们仔细看看上一条轨迹。 也许这将有助于确定库。

     /** * 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 java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1表示mPool是大小为10的数组,但mPoolSize=-1

    好的, 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值的唯一方法是通过mPoolSize=0减小它。 但是在条件mPoolSize > 0怎么办呢?

    我们将在Android Studio中设置断点,并查看应用程序启动时会发生什么。 我的意思是,这是if条件,此代码应该可以正常工作!

    最后,一个启示!



    请参阅DynamicFromMapSimplePool DynamicFromMap静态链接。

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

    在精心设置了断点的“播放”按钮上单击了数十次之后,我们看到mqt_native_modules线程使用React Native来控制React组件的样式属性(在组件width属性下面),调用SimplePool.acquireSimplePool.release函数。



    但是它们也可以被主流main访问!



    在上面,我们看到它们用于更新主流中的fill属性,通常用于react-native-svg组件! 实际上, react-native-svg仅在第7版中开始使用DynamicFromMap 以提高本机svg动画的性能。

    可以在两个线程中调用一个函数,但DynamicFromMap不会SimplePool线程安全的方式使用SimplePool 。 “线程安全”,说?

    线程安全,有点理论


    在单线程JavaScript中,开发人员通常不需要处理线程安全性。

    另一方面,Java支持并行或多线程程序的概念。 多个线程可以在同一程序中运行,并且可以潜在地访问常规数据结构,这有时会导致意外结果。

    举一个简单的例子:下图显示流A和B是并行的:

    • 读取整数
    • 增加其价值;
    • 还给他。


    在流A更新数据流之前,流B可能会访问该数据值。 我们期望两个单独的步骤最终得出19值。 相反,我们可以得到18 。 数据的最终状态取决于流操作的相对顺序的这种情况称为竞争条件。 问题是这种情况不一定总是发生。 在上述情况下,也许线程B在继续增加该值之前还有另一项工作,这为线程A提供了足够的时间来更新该值。 这解释了随机性和无法再现故障。

    如果可以由多个线程同时执行操作而没有竞争条件的风险,则认为数据结构是线程安全的。

    当一个线程读取特定的数据元素时,另一线程不应该有权修改或删除该元素(这称为原子性)。 在前面的示例中,如果更新周期是原子的,则可以避免争用条件。 线程B将等待,直到线程A完成操作,然后自行启动。

    在我们的情况下,这可能会发生:



    由于DynamicFromMap包含一个指向SimplePool的静态链接,因此在SimplePool ,调用多个DynamicFromMap调用来自不同的线程。

    在上图中,线程A调用了该方法,将条件评估为true ,但尚未设法减小mPoolSize的值(与线程B结合使用),而线程B也调用了此方法并将条件评估为true 。 随后,每个调用将减小mPoolSize的值,从而导致“不可能”值。

    改正


    研究更正选项后,我们发现了一个尚未加入分支的对react-native池请求 -在这种情况下,它提供了线程安全性。



    然后,我们为用户推出了固定版本的React Native。 崩溃终于解决了,欢呼!


    因此,感谢Jenick Duplessis(React Native核心贡献者)和Michael Sand( react-native-svg维护者)的帮助,该修补程序包含在React Native 0.57的下一个次要版本中

    需要花费一些时间来修复此错误,但这是一个深入研究react-native和react-native-svg的绝好机会。 一个好的调试器和一些适当放置的断点很重要。 希望您也从这个故事中学到了有用的东西!

Source: https://habr.com/ru/post/zh-CN432426/


All Articles