在 Android 中集成 React Native 的经验分享

在之前的一篇博客中,Allen已经为大家介绍了React Native在Glow的应用以及大体架构。由于React Native库本身的一些原因,其在Android的成熟度远不及iOS,因此也给在Android的应用带来了更多的挑战。

在本文中,给大家分享一下在Android平台上集成React Native的过程中碰到的一些问题和解决办法。

64位支持

目前React Native的二进制库还不支持64位,而Android并不支持32位和64位二进制库的混合加载(详见Mixing 32- and 64-bit Dependencies in Android)。 因此如果应用中已经包含了64位的二进制库,必须用abiFilters去掉64位二进制库。

ndk {  
    abiFilters "armeabi", "mips", "armeabi-v7a", "x86"
}

React Native社区也在努力解决这一问题(React Native for Android is incompatible with 3rd-party 64-bit libraries),目前看起来只有android-jsc这个依赖还没解决了,而且PR也已经有了,可以期待一下。

APK大小

React Native带来的另一个问题就是apk文件变大,仅32位的支持大概就会使APK增大8MB。对此比较敏感的话可以考虑使用多apk技术来解决,但因为会造成版本管理变得复杂,我们并未采用。

因为x86机型市场上比较少,我们曾经尝试过去掉对x86的支持,这样大概可以节省4MB的空间。但效果并不好,原因是Play Store似乎无法100%正确识别x86设备,造成在某些设备上下载了无法使用。也许这跟我们的应用是中途去掉了x86的支持有关,但无法验证,最后也只能放弃。

另外建议仔细阅读Google减小APK大小的建议Reduce APK Size,对减小APK尺寸也有不小的帮助。

JS堆栈记录

对于JS内部的错误捕获,我们使用的是Sentry + Raven-js的解决方案。但使用中发现Android系统上无法正确捕获JS堆栈记录,原因是Raven-js cannot correctly parse android stacktrace这个bug。最后的解决方案是在打包脚本中给Raven-js打一个补丁。

另外发现的一个问题是Android上的堆栈记录在某些情况下会产生偏移,而且只在minify过的JS代码上出现。这个问题目前还没有得到解决,幸好大多数情况下都可以根据异常本身的信息来找出正确的错误代码。如果有读者了解这个问题,还望不吝赐教。

ListView性能问题

在最初使用的React Native 0.42.3中,发现ListView在Android上有一个很严重的问题:ListView滚动结束后往往有一秒以上的时间内整个ListView停止响应,期间不能响应任何的点击事件。

花费大量时间调试后发现,ListView滚动过程中React Native的某个计算layout的函数占据了相当多的CPU时间。最后发现这也是React Native在0.40.0之后引入的一个bug,React Native做了过多的没有必要的layout计算,详见Extreme lag after upgrade to 0.39.2 and 0.40.0

通过升级React Native到0.44.0,问题得到了解决,但相关fix也导致了另外一个bug,这个在后面ViewPagerAndroid一节会讲到。

图片和内存

集成React Native后的第一个版本出现了不少crash,其中很大一部分的原因是内存不足。使用Android Studio自带的内存分析工具可以发现,在某些场景下有些图片占用了太多的内存(有的图片甚至可以达到20M)。进一步分析确定了原因:图片没有经过尺寸调整。我们的应用允许用户自己上传图片,而一旦某些图片尺寸比较大(注意不是文件大小,而是图片的长和宽),经过解码后就会占用很大的内存。

解决方案有两点:

  • 在服务端就返回调整过尺寸的图片,这样同时减少了网络开销。
  • 强制Image必须指定resizeMethod,对于尺寸和显示大小差不多的图片(例如图标),使用scale,对于可能超出显示尺寸很多的图片则一定要用resize来减少内存开销(具体说明可以参考React Native的官方文档)。

为了防止以后的开发过程中遗漏resizeMethod,我们定义了如下两个组件来代替原生的Image组件。

export function ResizeImage({...props}: Object): ReactNative.ReactElement {  
    return <ReactNative.Image resizeMethod={'resize'} {...props} />;
}
export function ScaleImage({...props}: Object): ReactNative.ReactElement {  
    return <ReactNative.Image resizeMethod={'scale'} {...props} />;
}

减少应用Crash

在刚才提到的crash中,另一部分是React Native自身引起的,而且用Monkey Test工具可以部分重现。通过在React Native加log调试可以发现,ShadowNodeRegistry会被多个线程访问,但没有做保护。简单的加入synchronized关键字可以修复很大一部分的crash。

具体代码参见Make ShadowNodeRegistry thread safe

通过这两个fix,各个应用的crash free rate还是很好的得到的保持

ViewPagerAndroid

在开发过程中发现的另一个问题是,给ViewPagerAndroid动态添加页面后,新的页面不会显示。调试后发现这个问题实际上跟ListView的性能fix有关,性能优化用力过猛,导致ViewPagerAndroid应该进行的layout计算也被省略了。

比较详细的说明和示例代码都可以在React Native v0.43 ViewPagerAndroid work not well when detached then attach中找到。

为此我们的解决方案是将ViewPagerAndroid绕开layout优化,代码可以参考Fix ReactViewPager layouting

总结

  • React Native在Android平台上的问题确实要比iOS上多一些
  • 遇到问题首先可以搜一下官方的issue列表,看看是不是已知问题,可以节省大量时间
  • 建议fork一个分支出来把React Native的Android库发布到私有仓库中,必要时候可以自行修复一些简单的问题
  • React Native的代码开发时就应该在Android和iOS上同时进行,避免一方不兼容造成的返工

最后,如果有什么问题欢迎留言来信交流。