写点什么

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

2018 年 12 月 12 日

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

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("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 的静态引用。


在精心设置断点并点了几十次 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


2018 年 12 月 12 日 14:521284
用户头像

发布了 731 篇内容, 共 360.8 次阅读, 收获喜欢 1829 次。

关注

评论 1 条评论

发布
暂无评论
发现更多内容

问题篇:WSL和VMware。你怎么选择(附wsl安装步骤)

小Q

Java Linux 学习 架构 面试

求职时这样回答问题你就输了!来自IT类面试官视角的深度解读

Java架构师迁哥

15张图解Redis为什么这么快

Java架构师迁哥

算法题解:Excel 工作表列标题

欧雷

Java 算法

甲方日常 40

句子

工作 随笔杂谈 日常

通过GUI界面更改 Ubuntu 20 LTS apt 源为阿里云

jiangling500

ubuntu 阿里云 apt

假的数字人民币钱包已出现,真的是啥样?

CECBC区块链专委会

数字货币 数字钱包

面试官:面对千万级、亿级流量怎么处理?

艾小仙

Java 缓存 分布式 高并发 中间件

如何实现微服务架构下的分布式事务?

华为云开发者社区

架构 分布式 事务

智能安防的普惠密码,在华为好望手中的三根“线头”上

脑极体

架构师训练营作业:第五周

m

USDT承兑商支付系统开发,USDT支付结算系统搭建

135深圳3055源中瑞8032

中台:未到终局,焉知生死?

ToB行业头条

中台

【JSRC小课堂】Web安全专题(二)逻辑漏洞的burpsuite插件开发

京东智联云开发者

Web

总结年初到10月底Java基础、架构面试题,共计1327道!涵盖蚂蚁金服、腾讯、字节跳动、美团、拼多多等等一线大厂!

Java架构追梦

Java 架构 字节跳动 面试 蚂蚁金服

VRBT视频彩铃解决方案

dwqcmo

5G 解决方案 实时音视频

LeetCode题解:78. 子集,递归回溯,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

保险区块链创新中心成立,三方面赋能行业数字化转型

CECBC区块链专委会

区块链 保险

《Maven实战》.pdf

田维常

程序员

week1 架构方法-作业-杨斌

杨斌

后李健熙时代的三星,将迎来怎样变局?

脑极体

快速掌握并发编程---线程池的原理和实战

田维常

程序员

.NET可视化权限功能界面设计

雯雯写代码

3年CRUD经验的Java程序员,金九银十想要跳槽,面试却遭到屡屡碰壁,感觉很迷茫!

Java成神之路

Java 程序员 架构 面试 编程语言

想了解Webpack,看这篇就够了

华为云开发者社区

华为 前端 开发

只有基于区块链才可能实现“大众创业、万众创新”

CECBC区块链专委会

区块链 分布式技术

调包侠的炼丹福利:使用Keras Tuner自动进行超参数调整

计算机与AI

学习 keras 超参数调优

Flink在窗口上应用函数-6-9

小知识点

scala 大数据 flink

端应用研发进入云原生时代

应用研发平台EMAS

合约一键跟单软件,API跟单软件开发

135深圳3055源中瑞8032

完美!Ali软件架构师下场“痛扁”spring源码,加入吗?

周老师

Java 编程 程序员 架构 面试

2021年,算法还“香”吗?

2021年,算法还“香”吗?

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