崩溃!去调试一个无法重现的错误?

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 版本上都会发生。

大多数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) {测试学习交流175317069君(qun)羊
            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 的绝佳机会。一个好的调试器和一些很好的断点很长的路要走。希望你也学到了一些有用的东西!

扩展阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容