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 的绝佳机会。一个好的调试器和一些很好的断点很长的路要走。但愿你也学到了一些有用的东西!