如何调试一个无法重现的错误?

2018年10月10日,我们的团队发布了一个新版本的React Native应用程序。我们很高兴又为我们的用户交付了新功能。

但是,恐怖的事情发生了!

发布几个小时后,我们突然收到很多Android崩溃事件。

\"\"

Android版本上发生了10000次崩溃

我们的崩溃报告工具Sentry像着火了一样!

所有的新错误都是类似“JSApplicationIllegalArgumentException Error while updating property ‘left’ in shadow node of type: RCTView”这样的。

在React Native中,如果你使用错误的类型设置属性,通常会发生这种情况。但是,为什么我们在测试应用程序时没有发现这个错误?我们的新版本已经在多个设备上测试过了。

此外,错误似乎是随机的,似乎在遇到属性和阴影节点类型的组合时会发生这个错误。以下是其中的3个错误:

\"\"

根据Sentry的报告,这些错误似乎在任意设备和任意Android版本上都会发生。

\"\"

重现错误

修复错误的第一步是重现错误。所幸的是,因为有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%的用户;

  • 与这些用户确认,看看新版本有没有发生崩溃。这样就可以验证我们的假设。

\"\"

要回退哪个库呢?

一种办法是通过抛硬币来决定,但我们真的要这么做吗?

再深入一些

好吧,让我们深入挖掘之前的堆栈跟踪信息,看看是否可以确定选择回退哪个库。

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(\u0026quot;Already in the pool!\u0026quot;);        }        if (mPoolSize \u0026lt; 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 \u0026gt; 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 \u0026gt; 0时,这种情况怎么可能会发生呢?

我们在Android Studio中设置了一个断点,并检查启动应用程序时发生了什么。我的意思是,因为有一个if条件,这段代码不应该会出现故障!

出乎意料!

DynamicFromMap持有对SimplePool的静态引用。

在精心设置断点并点了几十次Play按钮后,我们发现,mqt_native_modules线程调用了SimplePool.acquire和SimplePool.release(React Native用来管理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的绝佳机会。

英文原文:

https://blog.bam.tech/developper-news/debugging-a-non-reproducible-crash

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值