阅读 412

Airbnb 的 React Native 历程(二):技术篇

原文链接:medium.com/airbnb-engi…

我们在这个系列的 5 篇文章里,讲述了 Airbnb 使用 React Native 进行移动端开发的历程,以及在放弃 React Native 之后的计划。这是这个系列文章的第 2 篇。

This is the second in a series of blog posts in which we outline our experience with React Native and what is next for mobile at Airbnb.

在 Android、iOS、web 和跨平台框架交叉领域中,React Native 是一个相对较新且迅速发展的平台。在两年的实践后,我们可以大胆地说,React Native 在很多方面是革命性的。对于移动端开发来说,这是一种范式的转变,我们能从它所主张的的很多方面获益。然而,在获益的过程中,也伴随着很多显著的痛点。

React Native itself is a relatively new and fast-moving platform in the cross-section of Android, iOS, web, and cross-platform frameworks. After two years, we can safely say that React Native is revolutionary in many ways. It is a paradigm shift for mobile and we were able to reap the benefits of many of its goals. However, its benefits didn’t come without significant pain points.

哪些方面奏效了

What Worked Well

跨平台

Cross-Platform

React Native 首要的好处是你写得代码能够同时以 Native 的形式在 Android 和 iOS 上运行。使用 React Native 实现的大部分的功能都能够复用 95% 到 100% 的代码,只有 0.2% 的文件是平台特有的(.android.js 或者 .ios.js)。

The primary benefit of React Native is the fact that code you write runs natively on Android and iOS. Most features that used React Native were able to achieve 95–100% shared code and 0.2% of files were platform-specific (.android.js/.ios.js).

统一的设计语言系统(DLS)

Unified Design Language System (DLS)

我们开发了一门跨平台的设计语言,叫做 DLS。对于每一个 UI 组件,我们有 Android、iOS、React Native 和 web 的版本。有一门统一的设计语言和适合编写跨平台的功能,因为这意味着设计、组件名称和屏幕上的展现在不同的平台上都是一致的。同时,我们也能够在适当的情况下做一些适应不同平台的决策。比如,在 Android 上我们使用原生的 Toolbar,在 iOS 上则使用 UINavigationBar,另外我们在 Android 上隐藏 disclosure indicators("右箭头"指示器),因为它并不遵循 Android 平台的设计规范。

We developed a cross-platform design language called DLS. We have Android, iOS, React Native, and web versions of every component. Having a unified design language was amenable to writing cross-platform features because it meant that designs, component names, and screens were consistent across platforms. However, we were still able to make platform-appropriate decisions where applicable. For example, we use the native Toolbar on Android and UINavigationBar on iOS and we chose to hide disclosure indicators on Android because they don’t adhere to the Android platform design guidelines.

我们选择了在 React Native 上重写各个组件而不是封装原生组件,因为给各个平台单独提供适合平台的 API 会更加可靠,同时因为 Android 和 iOS 工程师可能不知道怎样正确地测试 React Native 的更改,因此这么做也降低了维护成本。然而,这也造成了平台间的碎片化,同一个组件的原生版本和 React Native 版本可能会不同步。

We opted to rewrite components instead of wrapping native ones because it was more reliable to make platform-appropriate APIs individually for each platform and reduced the maintenance overhead for Android and iOS engineers who may not know how to properly test changes in React Native. However, it did cause fragmentation between the platforms in which native and React Native versions of the same component would get out of sync.

React

React

React 之所以是最受喜爱的 web 框架,这是有原因的。它简单并且强大,能很好地扩展到大型的代码库。其中几个我们尤其喜欢的点是:

  • 组件化: React 通过良好定义的属性和状态强制要求分离关注点(UI 和业务逻辑)。这是 React 具有良好的可扩展性的主要原因。
  • 简单的生命周期: 众所周知,Android 和 iOS(稍微好一点)的生命周期都非常复杂。React 组件从根本上解决了这个问题,并且让学习 React Native 比学习 Android 或 iOS 简单得多。
  • 声明式: React 的声明式特性使得 UI 和底层的状态保持同步。

There is a reason that React is the most-loved web framework. It is simple yet powerful and scales well to large codebases. Some of the things we particularly like are:

  • Components: React Components enforce separation of concerns with well-defined props and state. This is a major contributor to React’s scalability.
  • Simplified Lifecycles: Android and, to a slightly lesser extent, iOS lifecycles are notoriously complex. Functional reactive React components fundamentally solve this problem and made learning React Native dramatically simpler than learning Android or iOS.
  • Declarative: The declarative nature of React helped keep our UI in sync with the underlying state.

迭代速度

Iteration Speed

使用 React Native 进行开发的时候,通过热加载(hot reloading),我们只需要一两秒钟就能在 Android 和 iOS 上看到代码的修改。对我们的原生 APP 来说,构建时的性能一直是头等的优先级,但从来都没有接近过我们使用 React Native 时的速度。最好的时候,原生 APP 的编译时间是 15 秒,但一次完整的打包时间可能高达 20 分钟。

While developing in React Native, we were able to reliably use hot reloading to test our changes on Android and iOS in just a second or two. Even though build performance is a top priority for our native apps, it has never come close to the iteration speed we achieved with React Native. At best, native compilation times are 15 seconds but can be as high as 20 minutes for full builds.

基础架构的投入

Investing in Infrastructure

在原生 APP 的基础架构层,我们集成了庞大的能力。所有的核心模块,例如网络、国际化(多语言)、A/B Test、共享元素转场动画、设备信息、账号信息等等,这些能力全都被封装到一个 React Native API 里。这些桥梁(bridges)是一些更复杂的部分,因为我们想把现有 Android 和 iOS 的 API 封装成对 React 来说是一致且规范的接口。随着原生基础架构的快速迭代和发展,让这些桥梁始终保持最新的状态,是一个不断追赶的过程,在这个过程中,基础架构团队的投入使得产品团队的工作更加容易。

We developed extensive integrations into our native infrastructure. All core pieces such as networking, i18n, experimentation, shared element transitions, device info, account info, and many others were wrapped in a single React Native API. These bridges were some of the more complex pieces because we wanted to wrap the existing Android and iOS APIs into something that was consistent and canonical for React. While keeping these bridges up to date with the rapid iteration and development of new infrastructure was a constant game of catch up, the investment by the infrastructure team made product work much easier.

没有这些基础架构上的投入,React Native 的开发体验和用户体验就会欠佳。因此,我们认为如果没有基础架构上大量持续的投入,React Native 就没法简单地被应用于现有 APP 的开发。

Without this heavy investment in infrastructure, React Native would have led to a subpar developer and user experiences. As a result, we don’t believe React Native can be simply tacked on to an existing app without a significant and continuous investment.

性能

Performance

人们对 React Native 最大的担忧之一是它的性能。然而,实际上这并不是一个问题。我们大部分 React Native 的界面和原生的界面一样流畅。人们通常认为性能只是一个单一的维度。经常有移动端工程师看到 JS 时就想着 “比 Java 慢”。然而,React Native 把业务逻辑和布局的过程移出主线程,实际上能够在很多情况下提升界面渲染的性能。

One of the largest concerns around React Native was its performance. However, in practice, this was rarely a problem. Most of our React Native screens feel as fluid as our native ones. Performance is often thought of in a single dimension. We frequently saw mobile engineers look at JS and think “slower than Java”. However, moving business logic and layout off of the main thread actually improves render performance in many cases.

有时我们确实会遇到性能的问题,这些问题通常是由过度渲染造成的,并且可以通过有效地使用 shouldComponentUpdateremoveClippedSubviews 及更好地利用 Redux 来缓解。

When we did see performance issues, they were usually caused by excessive rendering and were mitigated by effectively using shouldComponentUpdate, removeClippedSubviews, and better use of Redux.

然而,初始化和首次渲染时间使得 React Native 在启动界面(如下所述)、deeplinks 等方面表现较差,并提高了界面之间切换的 TTI(Time To Interactive)。另外,由于 Yoga 把 React Native 组件转成了原生的 View,界面绘制时的掉帧很难调试。

However, the initialization and first-render time (outlined below) made React Native perform poorly for launch screens, deeplinks, and increased the TTI time while navigating between screens. In addition, screens that dropped frames were difficult to debug because Yoga translates between React Native components and native views.

Redux

Redux

我们使用 Redux 实现状态管理,我们发现 Redux 很高效,并且能够防止 UI 和 状态不同步,以及很容易实现不同界面间的数据共享。然而,Redux 因为它的 boilerplate 脚手架而臭名昭著,并且它的学习曲线相对较难。我们为一些通用的模板提供了生成器,但这依然是使用 React Native 时的一大难题以及困惑来源。但这些难题并不是 React Native 特有的,所以并不值得强调。

We used Redux for state management which we found effective and prevented the UI from ever getting out of sync with state and enabled easy data sharing across screens. However, Redux is notorious for its boilerplate and has a relatively difficult learning curve. We provided generators for some common templates but it was still one of the most challenging pieces and source of confusion while working with React Native. It is worth noting that these challenges were not React Native specific.

原生的支持

Backed by Native

因为 React Native 的所有东西都能通过 bridge 调用原生代码,我们最终实现了很多我们一开始不确定是否可行的东西,比如:

  1. 共享元素转场动画: 我们开发了一个 组件,这个组件实际是由 Android 和 iOS 原生的 shared element 代码实现的。这个组件甚至能够在原生界面和 React Native 界面的切换时使用。
  2. Lottie: 通过封装 Android 和 iOS 上原有的库,我们让 Lottie 能够在 React Native 上正常运作。
  3. 原生的网络框架: React Native 使用我们原有的原生网络框架,并且在原生和 React Native 上都能使用缓存。
  4. 其他核心基础: 就像网络框架一样,我们封装了其余的原生基础框架,比如国际化(多语言)、A/B Test 框架等等,使得他们能在 React Native 上无缝地运作。

Because everything in React Native can be bridged by native code, we were ultimately able to build many things we weren’t sure were possible at the beginning such as:

  1. Shared element transitions: We built a component that is backed by native shared element code on Android and iOS. This even works between native and React Native screens.
  2. Lottie: We were able to get Lottie working in React Native by wrapping the existing libraries on Android and iOS.
  3. Native networking stack: React Native uses our existing native networking stack and cache on both platforms.
  4. Other core infra: Just like networking, we wrapped the rest of our existing native infrastructure such as i18n, experimentation, etc. so that it worked seamlessly in React Native.

静态分析

Static Analysis

在 Web 端,我们长期深度地使用 ESLint。但在 Airbnb,我们是第一个使用 Prettier 的平台(译者注:ESLint 和 Prettier 都是静态代码分析工具)。我们发现,在 PR(Pull Request)的时候,Prettier 对于减少 nits 和 bikeshedding(译者注:nits 和 bikeshedding 指不严重的、不影响代码正确运行的问题,如代码格式等)很有效。我们 Web 端的基础架构团队正在深入调研 Prettier。

We have a strong history of using eslint on web which we were able to leverage. However, we were the first platform at Airbnb to pioneer prettier. We found it to be effective at reducing nits and bikeshedding on PRs. Prettier is now being actively investigated by our web infrastructure team.

同时,我们也分析测量渲染时间和性能,进而找出哪些界面应该优先深入研究,去找出其中的性能问题。

We also used analytics to measure render times and performance to figure out which screens were the top priority to investigate for performance issues.

相比我们的 Web 端的基础架构,React Native 更小,也更新,因此它已被证明是一个测试新 idea 的良好平台。很多我们为 React Native 打造的工具和 idea 都已经被 Web 端采用。

Because React Native was smaller and newer than our web infrastructure, it proved to be a good testbed for new ideas. Many of the tools and ideas we created for React Native are being adopted by web now.

动画

Animations

多亏 React Native Animated 库,我们得以实现顺滑的动画,甚至是交互驱动的动画,比如滚动时的视差效果。

Thanks to the React Native Animated library, we were able to achieve jank-free animations and even interaction-driven animations such as scrolling parallax.

JS/React 开源

JS/React Open Source

因为 React Native 实际运行的是 React 和 JavaScript,我们得以使用 Javascript 海量的开源项目,比如 Redux、Reselect、Jest 等等。

Because React Native truly runs React and javascript, we were able to leverage the extremely vast array of javascript projects such as redux, reselect, jest, etc.

Flexbox

Flexbox

React Native 使用 Yoga 来处理布局,这是一个跨平台的 C 语言库,它通过 Flexbox 的 API 处理布局的计算。早期,我们收到 Yoga 的一些限制,比如它不支持长宽比,但这个已经在后续的更新里添加了支持。另外,一些有趣的教程,比如 Flexbox froggy,使得入门更加有趣。

React Native handles layout with Yoga, a cross-platform C library that handles layout calculations via the flexbox API. Early on, we were hit with Yoga limitations such as the lack of aspect ratios but they have been added in subsequent updates. Plus, fun tutorials such as flexbox froggy made onboarding more enjoyable.

和 Web 端的合作

Collaboration with Web

在探索 React Native 的后期,我们开始针对 Web、iOS 和 Android 进行构建。因为 Web 也使用 Redux,我们发现大量的代码无需修改就可以在 Web 和 原生 APP 间共享。

Late in the React Native exploration, we began building for web, iOS, and Android at once. Given that web also uses Redux, we found large swaths of code that could be shared across web and native platforms with no alterations.

哪些方面不够理想

What didn’t work well

React Native 的不成熟

React Native Immaturity

相比 Android 和 iOS,React Native 还不够成熟。它狠心、野心很大,并且发展迅猛。虽然在大多数情况下,React Native 表现都很好,但是在某些情况下,React Native 的不成熟还是会表现出来,并导致一些在原生开发里很容易实现的东西变得很困难。不幸的是,这些情况很难预测,并且可能会花费几个小时到几天的时间去解决。

React Native is less mature than Android or iOS. It is newer, highly ambitious, and moving extremely quickly. While React Native works well in most situations, there are instances in which its immaturity shows through and makes something that would be trivial in native very difficult. Unfortunately, these instances are hard to predict and can take anywhere from hours to many days to work around.

维护 React Native 代码库的一个 Fork`

Maintaining a Fork of React Native

由于 React Native 的不成熟,有时候我们需要对 React Native 的资源打补丁。除了给 React Native 做贡献之外,我们不得不维护一个 fork,这样我们才能快速合入我们的修改并升级版本。过去的两年来,我们不得不在 React Native 的官方项目之上加了大概 50 个 commit。这个情况导致我们升级 React Native 的过程异常痛苦。

Due to React Native’s immaturity, there were times in which we needed to patch the React Native source. In addition to contributing back to React Native, we had to maintain a fork in which we could quickly merge changes and bump our version. Over the two years, we had to add roughly 50 commits on top of React Native. This makes the process of upgrading React Native extremely painful.

JavaScript 工具

JavaScript Tooling

JavaScript 是一门无类型的语言。缺乏类型安全既导致它难以扩展,也成为一些习惯于有类型语言的移动端工程师的争论点,不然这些工程师对学习 React Native 挺感兴趣的。我们探索过它的 Adopting Flow,但是它隐蔽的错误信息导致了令人沮丧的开发体验。我们也探索过 TypeScript,但是把它集成到我们现有的基础架构(比如 bable 和 metro bundler)已被证明是很有问题的。但是,我们还在继续积极地在 Web 端探索 TypeScript。

JavaScript is an untyped language. The lack of type safety was both difficult to scale and became a point of contention for mobile engineers used to typed languages who may have otherwise been interested in learning React Native. We explored adopting flow but cryptic error messages led to a frustrating developer experience. We also explored TypeScript but integrating it into our existing infrastructure such as babel and metro bundler proved to be problematic. However, we are continuing to actively investigate TypeScript on web.

重构

Refactoring

JavaScript 作为无类型语言的一个副作用是,重构会非常困难且又容易出错。重命名属性,尤其是对于那些名字很通用的(比如 onClick)的属性,这些属性又在多个组件间传递的时候,想要准确地完成这种重构简直就是噩梦。更糟糕地是,这种错误在线上版本出错,而无法在编译时就发现这种错误,而且很难增加合适地静态分析。(译者注:所谓“动态类型一时爽,代码重构火葬场”。)

A side-effect of JavaScript being untyped is that refactoring was extremely difficult and error-prone. Renaming props, especially props with a common name like onClick or props that are passed through multiple components were a nightmare to refactor accurately. To make matters worse, the refactors broke in production instead of at compile time and were hard to add proper static analysis for.

JavaScriptCore 不一致

JavaScriptCore inconsistencies

React Native 一个微妙而棘手地地方在于,它是在 JavaScriptCore 地环境下运行的。以下是我们由此遇到的一下问题:

  • iOS 系统自带了 JavaScriptCore,这意味着 iOS 上的 JavaScriptCore 通常是一致的,而且对我们来说一般不会出现问题。
  • 由于 Android 系统并不自带 JavaScriptCore,因此 ReactNative 需要打包带上。因为默认打包的是一个古老的版本,因此我们需要花精力去打包一个更新的版本。
  • 调试的时候,React Native 连接到一个 Chrome Developer Tools 实例。这非常好,因为那是一个非常强大的调试器。然而,当连接了这个调试器之后,所有的 JavaScript 就在 Chrome 的 V8 引擎下运行,在 99.9% 的情况下,这是没问题的。但是这里举一个有问题的例子,toLocaleString 在 iOS 上运行没有问题,但是在 Android 上只有在调试的时候才能正常运行。这证明 Android 的 JSC 并不支持这个函数并且自动失败,除非是在 V8 的环境下调试的时候才能正常运行。对产品开发的工程师来说,如果不了解这种技术细节,可能得花上几天的时间进行痛苦的调试。

One subtle and tricky aspect of React Native is due to the fact that it is executed on a JavaScriptCore environment. The following are consequences we encountered as a result:

  • iOS ships with its own JavaScriptCore out of the box. This meant that iOS was mostly consistent and not problematic for us.
  • Android doesn’t ship its own JavaScriptCore so React Native bundles its own. However, the one you get by default is ancient. As a result, we had to go out of our way to bundle a newer one.
  • While debugging, React Native attaches to a Chrome Developer Tools instance. This is great because it is a powerful debugger. However, once the debugger is attached, all JavaScript runs within Chrome’s V8 engine. This is fine 99.9% of the time. However, in one instance, we got bit when toLocaleString worked on iOS but only worked on Android while debugging. It turns out that the Android JSC doesn’t include it and it was silently failing unless you were debugging in which case it was using V8 which does. Without knowing technical details like this, it can lead to days of painful debugging for product engineers.

React Native 开源库

React Native Open Source Libraries

学习一个平台是既困难又花时间的。很多人只熟悉一到两个平台。一些提供原生桥梁(native bridges,比如地图,视频等)的 React Native 库,要求同时同等地熟悉 3 个平台才能够成功运用好这些库。我们发现,很多 React Native 的开源项目,都是由一些只在一到两个平台上有过经验的人编写的。这导致了这些库在 Android 和 iOS 上的不一致性及一些不符合预期的 bug。

Learning a platform is difficult and time-consuming. Most people only know one or two platforms well. React Native libraries that have native bridges such as maps, video, etc. requires equal knowledge of all three platforms to be successful. We found that most React Native Open source projects were written by people who had experience with only one or two. This led to inconsistencies or unexpected bugs on Android or iOS.

在 Android 上,很多 React Native 库也要求使用一个到 node_modules 的相对路径进行依赖,而不是把这些库发不到 Maven 仓库上,这个是不符合 Android 社区规范的。

On Android, many React Native libraries also require you to use a relative path to node_modules rather than publishing maven artifacts which are inconsistent with what is expected by the community.

基础架构和产品功能的并行开发

Parallel Infrastructure and Feature Work

在 Android 和 iOS 上,我们已经积累了很多年的基础架构建设。但是对于 React Native,我们是从头开始,并且不得不为现有的基础架构构建很多桥梁(bridges)。这就意味着,有时候产品工程师需要使用某些基础功能,但这些功能在 React Native 上尚未支持。这种情况下,他们要么不得不在一个他们并不熟悉的平台上、及在他们的职能范围之外去构建这些功能,要么就等到这种功能被支持的时候再继续产品的开发。

We have accumulated many years of native infrastructure on Android and iOS. However, in React Native, we started with a blank slate and had to write or create bridges of all existing infrastructure. This meant that there were times in which a product engineer needed some functionality that didn’t yet exist. At that point, they either had to work in a platform they were unfamiliar with and outside the scope of their project to build it or be blocked until it could be created.

Crash 监控

Crash Monitoring

我们使用 Bugsnag 来上报 Android 和 iOS 上的 crash。尽管我们可以使Bugsnag在这两个平台上都能正常工作,但与在其他平台上相比,它的可靠性较低,需要的工作量也更多。因为 React Native 是相对较新的技术,而且在业界的使用还比较少,我们得自己构建很多基础设施,比如在内部上传资源的映射,以及和 Bugsnag 一起实现某些功能,比如过滤出那些在 React Native 里发生的 crash。

We use Bugsnag for crash reporting on Android and iOS. While we were able to get Bugsnag generally working on both platforms, it was less reliable and required more work than it did on our other platforms. Because React Native is relatively new and rare in the industry, we had to build a significant amount of infrastructure such as uploading source maps in-house and had to work with Bugsnag to be able to do things like filter crashes by just those that occurred in React Native.

因为我们在 React Native 之外还有自定义的基础架构,我们有时会遇到一些问题,比如 crash 没有上报,或者资源映射没有适当地上传。

Due to the amount of custom infrastructure around React Native, we would occasionally have serious issues in which crashes weren’t reported or source maps weren’t properly uploaded.

最后,如果造成 crash 的问题跨 React Native 和原生的代码,调试 React Native 的 crash 会很困难,因为堆栈(stack traces)无法跟踪 React Native 和原生之间的切换。

Finally, debugging React Native crashes were often more challenging if the issue spanned React Native and native code since stack traces don’t jump between React Native and native.

原生桥梁

Native Bridge

为了原生和 React Native 之前的通信,React Native 提供了一个接口(Bridge API)作为桥梁。这个接口可以正常工作,但是代码写起来非常麻烦。首先,它要求适当地配置所有的三个开发环境。我们也遇到很多 JavaScript 传过来的数据类型不符合预期的问题。比如,整形经常被包装成字符串,这个问题只有在数据通过 bridge 传递的时候才能发现。更糟的是,有时候 iOS 会自动失败但 Android 会直接 crash。我们在 2017 年底开始研究从 TypeScript 的定义自动生成 bridge 的代码,但为时已晚了。

React Native has a bridge API to communicate between native and React Native. While it works as expected, it is extremely cumbersome to write. Firstly, it requires all three development environments to be properly set up. We also experienced many issues in which the types coming from JavaScript were unexpected. For example, integers were often wrapped by strings, an issue that isn’t realized until it is passed over a bridge. To make matters worse, sometimes iOS will fail silently while Android will crash. We began to investigate automatically generating bridge code from TypeScript definitions towards the end of 2017 but it was too little too late.

初始化时间

Initialization Time

在 React Native 首次渲染前,你必须初始化它的运行时环境。不幸的是,对一个像我们这种大小的 APP,就算在一个高端手机上,运行时初始化也需要几秒钟的时间。这样的话,在 APP 启动的界面上使用 React Native 是不可能的。我们在 APP 启动的时候就初始化它的运行时,这样来减小它的首次渲染时间。

Before React Native can render for the first time, you must initialize its runtime. Unfortunately, this takes several seconds for an app of our size, even on a high-end device. This made using React Native for launch screens nearly impossible. We minimized the first-render time for React Native by initializing it at app-launch.

初始渲染时间

Initial Render Time

和原生界面不一样,渲染 React Native 需要至少一次完整的 “主线程 -> js -> yoga 布局线程 -> 主线程” 的往返时间,这样才能在第一次渲染界面的时候有足够的信息。据统计,iOS P90 的的初始渲染时间平均是 280 毫秒,Android 上是 440 毫秒。在 Android 上,我们使用了 postponeEnterTransition API 来实现界面的延迟展示(渲染完成后才展示),这个 API 一般被用在共享元素转场动画上。在 iOS 上,我们在 React Native 里想要足够快地配置导航条(navbar)的时候遇到了问题。因此,我们给所有 React Native 界面的切换都人工加上了 50 毫秒的延时,这样可以防止配置加载完成的时候导航条的闪烁。

Unlike with native screens, rendering React Native requires at least one full main thread -> js -> yoga layout thread -> main thread round trip before there is enough information to render a screen for the first time. We saw an average initial p90 render of 280ms on iOS and 440ms on Android. On Android, we used the postponeEnterTransition API which is normally used for shared element transitions to delay showing the screen until it has rendered. On iOS, we had issues setting the navbar configuration from React Native fast enough. As a result, we added an artificial delay of 50ms to all React Native screen transitions to prevent the navbar from flickering once the configuration was loaded.

APP 包大小

App Size

React Native 对 APP 包大小也有不可忽略的影响。在 Android 上,React Native 总体的大小是(Java + JS + 如 Yoga 之类的 native 库 + Javascript 运行时)8mb 每 ABI。如果是 x86 + arm(仅 32 位),大小大概是 12mb。

React Native also has a non-negligible impact on app size. On Android, the total size of React Native (Java + JS + native libraries such as Yoga + Javascript Runtime) was 8mb per ABI. With both x86 and arm (32 bit only) in one APK, it would have been closer to 12mb.

64 位

64-bit

因为这个原因,我们至今也无法上线 64 位的 APK。

We still can’t ship a 64-bit APK on Android because of this issue.

手势

Gestures

对于一些手势操作比较复杂的界面,我们避免使用 React Native,因为 Android 和 iOS 的触屏处理子系统很不一样,所以对整个 React Native 社区来说,提出一个统一的触屏处理 API 是很有挑战的。但是,这项工作还在继续,而且 react-native-gesture-handler 已经发布里 1.0 版本。

We avoided using React Native for screens that involved complex gestures because the touch subsystem for Android and iOS are different enough that coming up with a unified API has been challenging for the entire React Native community. However, work is continuing to progress and react-native-gesture-handler just hit 1.0.

长列表

Long Lists

在长列表方面,React Native 已经有了一些进展,比如像 FlatList 之类的库。但是,它们远未接近 Android 的 RecyclerView 和 iOS 的 UICollectionView 的成熟度和灵活性。因为线程管理的原因,很多限制是难以克服的。适配器(Adapter)的数据不能同步访问,因此快速滚动的时候,有可能看到 View 闪入,因为它们是异步渲染的。文本也不能同步测量,因此 iOS 上也不能通过预计算单元格高度的方式做一些特定的优化。

React Native has made some progress in this area with libraries like FlatList. However, they are nowhere near the maturity and flexibility of RecyclerView on Android or UICollectionView on iOS. Many of the limitations are difficult to overcome because of the threading. Adapter data can’t be accessed synchronously so it is possible to see views flash in as they get asynchronously rendered while scrolling quickly. Text also can’t be measured synchronously so iOS can’t make certain optimizations with pre-computed cell heights.

更新 React Native

Upgrading React Native

尽管大部分 React Native 的升级是微不足道的,但有些时候也会很痛苦。特别是,几乎不可能使用 React Native 的 0.43 版本(2017 年 4 月)到 0.49 版本(2017 年 10 月),因为它使用的是 React 16 的 alpha 和 beta 版本。这是一个很大的问题,因为很多专为 web 设计的 React 库不支持预发布的 React 版本。解决此升级的适当依赖项的过程对 2017 年中期的其他 React Native 基础架构工作造成了重大不利影响。

Although most React Native upgrades were trivial, there were a few that wound up being painful. In particular, it was nearly impossible to use React Native 0.43 (April 2017) to 0.49 (October 2017) because it used React 16 alpha and beta. This was hugely problematic because most React libraries that are designed for web use don’t support pre-release React versions. The process of wrangling the proper dependencies for this upgrade was a major detriment to other React Native infrastructure work in mid-2017.

无障碍功能(Accessibility)

Accessibility

在 2017 年,我们对无障碍功能做了一次大修,我们花了很大精力去让残障人士能使用 Airbnb 预定满足他们需求的房源。然而,在 React Native 的无障碍功能 API 里还有很多漏洞。为了达到无障碍功能的最低标准,我们也不得不维护自己的 React Native 的 fork 以便合并修改。对于这种情况,在 Android 或 iOS 上一个一行代码的修改,需要话费几天的时间找出怎样驾到 React Native 上,cherry-pick 它,然后在 React Native core 上备案一个 issue,并且在接下来几周里去跟进。

In 2017, we did a major accessibility overhaul in which we invested significant efforts to ensure that people with disabilities can use Airbnb to book a listing that can accommodate their needs. However, there were many holes in the React Native accessibility APIs. In order to meet even a minimum acceptable accessibility bar, we had to maintain our own fork of React Native where we could merge fixes. For these case, a one-line fix on Android or iOS wound up taking days of figuring out how to add it to React Native, cherry picking it, then filing an issue on React Native core and following up on it over the coming weeks.

棘手的 Crash

Troublesome Crashes

我们不得不去处理一些奇怪又难修复的 crash。例如,我们当前正遇到这个关于 @ReactProp 注解的 crash,这个 crash 没法在任何设备上复现,就算在那些和发生 crash 的用户的设备硬件和软件都相同的设备上。

We have had to deal with a few very bizarre crashes that are hard to fix. For example, we are currently experiencing this crash on the @ReactProp annotation and have been unable to reproduce it on any device, even those with identical hardware and software to ones that are crashing in the wild.

Android 上跨进程的 SavedInstanceState

SavedInstanceState Across Processes on Android

Android 会频繁地清理后台进程,但是会给机会被清理的进程同步地保存它们的状态到一个 bundle 里。但是在 React Native 上,所有的状态都只有在 JS 线程才能被访问到,所以保存状态不能同步地进行。就算情况不是这样,Redux 作为一个状态保存者也没法和这个策略兼容,因为它同时包含了可序列化和不可序列化的数据,也可能包含超出 savedInstanceState bundle 可容纳的数据,这将会导致线上的 crash

Android frequently cleans up background processes but gives them a chance to synchronously save their state in a bundle. However, on React Native, all state is only accessible in the js thread so this can’t be done synchronously. Even if this weren’t the case, redux as a state store is not compatible with this approach because it contains a mix of serializable and non-serializable data and may contain more data than can fit within the savedInstanceState bundle which would lead to crashes in production.


这是这个系列文章的第二部分,这个系列重点讲述了 React Native 在 Airbnb 的历程,以及 Airbnb 在此后的计划。

This is part two in a series of blog posts highlighting our experiences with React Native and what’s next for mobile at Airbnb.