React Native 从入门到原理
1、本文分为两个部分:上半部分用通俗的语言解释了相关的名词,重点介绍 React Native 出现的背景和试图解决的问题。适合新手对 React Native 形成初步了下半部分则通过源码(0.27 版本)分析 React Native 的工作原理,适合深入学习理解 React Native 的运行机制。最后则是我个人对 React Native 的分析与前景判断。
2、动态配置由于 AppStore 审核周期的限制,如何动态的更改 app 成为了永恒的话题。无论采用何种方式,我们的流程总是可以归结为以下三部曲:“从 Server 获取配置 --> 解析 --> 执行native代码”。很多时候,我们自觉或者不自觉的利用 JSON 文件实现动态配置的效果,它的核心流程是:通过 HTTP 请求获取 JSON 格式的配置文件。配置文件中标记了每一个元素的属性,比如位置,颜色,图片 URL 等。解析完 JSON 后,我们调用 Objective-C 的代码,完成 UI 控件的渲染。通过这种方法,我们实现了在后台配置 app 的展示样式。从本质上来说,移动端和服务端约定了一套协议,但是协议内容严重依赖于应用内要展示的内容,不利于拓展。也就是说,如果业务要求频繁的增加或修改页面,这套协议很难应付。最重要的是,JSON 只是一种数据交换的格式,说白了,我们就是在解析文本数据。这就意味着它只适合提供一些配置信息,而不方便提供逻辑信息。举个例子,我们从后台可以配置颜色,位置等信息,但如果想要控制 app 内的业务逻辑,就非常复杂了。记住,我们只是在解析字符串,它完全不具备运行和调试的能力。
3、React不妨暂时抛弃移动端的烦恼,来看看前端的“新玩意”。
4、背景作为前端小白,我以前对前端的理解是这样的:用 HTML 创建 DOM,构建整个网页的布局、结构用 CSS 控制 DOM 的样式,比如字体、字号、颜色、居中等用 JavaScript 接受用户事件,动态的操控 DOM在这三者的配合下,几乎所有页面上的功能都能实现。但也有比较不爽地方,比如我想动态修改一个按钮的文字,我需要这样写:

6、可以看到,在 HTML 和 JavaScript 代码中,id和onclick事件触发的函数必须完全对应,否则就无法正确的响应事件。如果想知道一个 HTML 标签会如何被响应,我们还得跑去 JavaScript 代码中查找,这种原始的配置方式让我觉得非常不爽。初识 React随着 FaceBook 推出了 React 框架,这个问题得到了大幅度改善。我们可以把一组相关的 HTML 标签,也就是 app 内的 UI 控件,封装进一个组件(Component)中,我从阮一峰的 React 教程中摘录了一段代码:

8、上文已经解释过了何谓“魑徒扮阙简洁的语法”,因为我们可以暂时放下 HTML 和 CSS,只关心如何用 JavaScript 构造页面。所谓的“高效”,是因为 React 割飧褡迨独创了 Virtual DOM 机制。Virtual DOM 是一个存在于内存中的 JavaScript 对象,它与 DOM 是一一对应的关系,也就是说只要有 Virtual DOM,我们就能渲染出 DOM。当界面发生变化时,得益于高效的 DOM Diff 算法,我们能够知道 Virtual DOM 的变化,从而高效的改动 DOM,避免了重新绘制 DOM。当然,React并不是前端开发的全部。从之前的描述也能看出,它专注于 UI 部分,对应到 MVC 结构中就是 View 层。要想实现完整的 MVC 架构,还需要 Model 和 Controller 的结构。在前端开发时,我们可以采用 Flux 和 Redux 架构,它们并非框架(Library),而是和 MVC 一样都是一种架构设计(Architecture)。如果不从事前端开发,就不用深入的掌握 Flux 和 Redux 架构,但理解这一套体系结构对于后面理解 React Native非常重要。React Native分别介绍完了移动端和前端的背景知识后,本文的主角——React Native 终于要登场了。融合前面我们介绍了移动端通过 JSON 文件传递信息的不足之处:只能传递配置信息,无法表达逻辑。从本质上讲,这是因为 JSON 毕竟只是纯文本,它缺乏像编程语言那样的运行能力。而 React 在前端取得突破性成功以后,JavaScript 布道者们开始试图一统三端。他们利用了移动平台能够运行 JavaScript 代码的能力,并且发挥了 JavaScript 不仅仅可以传递配置信息,还可以表达逻辑信息的优点。当痛点遇上特点,两者一拍即合,于是乎:

10、而非很多跨平台语言,项目所说的:

13、这里的JSConte垓矗梅吒xt指的是 JavaScript 代码的运行环境,通过evaluateScript即可执行 JavaScript 代码并获取返回结果。JavaScript 是一种单线程的语言,它不具备自运行的能力,因此总是被动调用。很多介绍 React Native 的文章都会提到 “JavaScript 线程” 的概念,实际上,它表示的是 Objective-C 创建了一个单独的线程,这个线程只用于执行 JavaScript 代码,而且 JavaScript 代码只会在这个线程中执行。Objective-C 与 JavaScript 交互提到 Objective-C 与 JavaScript 的交互,不得不推荐 bang神的这篇文章:React Native通信机制详解。虽然其中不少细节都已经过时,但是整体的思路值得学习。本节主要分析 Objective-C 与 JavaScript 交互时的整理逻辑与流程,下一节将通过源码来分析具体原理。JavaScript 调用 Objective-C由于 JavaScript Core 是一个面向 Objective-C 的框架,在 Objective-C 这一端,我们对 JavaScript 上下文知根知底,可以很容易的获取到对象,方法等各种信息,当然也包括调用 JavaScript 函数。真正复杂的问题在于,JavaScript 不知道 Objective-C 有哪些方法可以调用。React Native 解决这个问题的方案是在 Objective-C 和 JavaScript 两端都保存了一份配置表,里面标记了所有 Objective-C 暴露给 JavaScript 的模块和方法。这样,无论是哪一方调用另一方的方法,实际上传递的数据只有ModuleId、MethodId和Arguments这三个元素,它们分别表示类、方法和方法参数,当 Objective-C 接收到这三个值后,就可以通过 runtime 唯一确定要调用的是哪个函数,然后调用这个函数。再次重申,上述解决方案只是一个抽象概念,可能与实际的解决方案有微小差异,比如实际上 Objective-C 这一端,并没有直接保存这个模块配置表。具体实现将在下一节中随着源码一起分析。闭包与回调既然说到函数互调,那么就不得不提到回调了。对于 Objective-C 来说,执行完 JavaScript 代码再执行 Objective-C 回调毫无难度,难点依然在于 JavaScript 代码调用 Objective-C 之后,如何在 Objective-C 的代码中,回调执行 JavaScript 代码。目前 React Native 的做法是:在 JavaScript 调用 Objective-C 代码时,注册要回调的 Block,并且把BlockId作为参数发送给 Objective-C,Objective-C 收到参数时会创建 Block,调用完 Objective-C 函数后就会执行这个刚刚创建的 Block。Objective-C 会向 Block 中传入参数和BlockId,然后在 Block 内部调用 JavaScript 的方法,随后 JavaScript 查找到当时注册的 Block 并执行。图解好吧,如果你是新手,并且坚持读到了这里,估计已经懵逼了。不要担心,与 JavaScript 的交互确实不是一下子能够完全理清楚的,你可以先参考这个示意图:

15、用户能看到的一切内容都来源于这个RootView,所有的初始化工作也都在这个方法内完成。在这个方法内部,在创建RootVi髫潋啜缅ew之前,React Native 实际上先创建了一个Bridge对象。它是 Objective-C 与 JavaScript 交互的桥梁,后续的方法交互完全依赖于它,而整个初始化过程的最终目的其实也就是创建这个桥梁对象。初始化方法的核心是setUp方法,而setUp方法的主要任务则是创建BatchedBridge。BatchedBridge的作用是批量读取 JavaScript 对 Objective-C 的方法调用,同时它内部持有一个JavaScriptExecutor,顾名思义,这个对象用来执行 JavaScript 代码。创建BatchedBridge的关键是start方法,它可以分为五个步骤:读取 JavaScript 源码初始化模块信息初始化 JavaScript 代码的执行器,即RCTJSCExecutor对象生成模块列表并写入 JavaScript 端执行 JavaScript 源码我们逐个分析每一步完成的操作:读取 JavaScript 源码这一部分的具体代码实现没有太大的讨论意义。我们只要明白,JavaScript 的代码是在 Objective-C 提供的环境下运行的,所以第一步就是把 JavaScript 加载进内存中,对于一个空的项目来说,所有的 JavaScript 代码大约占用 1.5 Mb 的内存空间。需要说明的是,在这一步中,JSX 代码已经被转化成原生的 JavaScript 代码。初始化模块信息这一步在方法initModulesWithDispatchGroup:中实现,主要任务是找到所有需要暴露给 JavaScript 的类。每一个需要暴露给 JavaScript 的类(也成为 Module,以下不作区分)都会标记一个宏:RCT_EXPORT_MODULE,这个宏的具体实现并不复杂:

17、因此,React Native 可以通过RCTModuleClasses拿到所有暴露给 JavaScript 的类。下一步操作是遍历这个数组,然后生成RCTModuleData对象:

19、因此 Objective-C 管理模块配置表的逻辑是:Bridge 持有一个数组,数组中保存了所有的模块的RCTModuleData对象。只要给定ModuleId和MethodId就可以唯一确定要调用的方法。初始化 JavaScript 代码的执行器,即RCTJSCExecutor对象通过查看源码可以看到,初始化 JavaScript 执行器的时候,addSynchronousHookWithName这个方法被调用了多次,它其实向 JavaScript 上下文中添加了一些 Block 作为全局变量:

21、这就是模块配置表能够加载到 JavaScript 中的原理。另一个值得关注的Block 叫做nativeFlushQueueImmediate。实际上,JavaScript 除了把调用信息放到 MessageQueue 中等待 Objective-C 来取以外,也可以主动调用 Objective-C 的方法:

23、这个handleBuffer方法是 JavaScript 调用 Objective-C 方法的关键,在下一节——方法调用中,我会详细分析它的实现原理。一般情况下,Objective-C 会定时、主动的调用handleBuffer方法,这有点类似于轮询机制:

25、然而由于卡顿或某些特殊原因,Objective-C 并不能总是保证能够准时的清空 MessageQueue,这就是为什么 JavaScript 也会在一定时间后主动的调用 Objective-C 的方法。查看上面 JavaScript 的代码可以发现,这个等待时间是 5ms。请牢牢记住这个 5ms,它告诉我们 JavaScript 与 Objective-C 的交互是存在一定开销的,不然就不会等待而是每次都立刻发起请求。其次,这个时间开销大约是毫秒级的,不会比 5ms 小太多,否则等待这么久就意义不大了。生成模块配置表并写入 JavaScript 端复习一下nativeRequireModuleConfig这个 Block,它可以接受 ModuleName 并且生成详细的模块信息,但在前文中我们没有提到 JavaScript 是如何知道 Objective-C 要暴露哪些类的(目前只是 Objective-C 自己知道)。这一步的操作就是为了让 JavaScript 获取所有模块的名字:

27、方法调用如前文所述,在 React Native 中,Objective-C 和 JavaScript 的交互都是通过传递ModuleId、MethodId和Arguments进行的。以下是分情况讨论:调用 JavaScript 代码也许你在其他文章中曾经多次听说 JavaScript 代码总是在一个单独的线程上面调用,它的实际含义是 Objective-C 会在单独的线程上运行 JavaScript 代码:

29、需要注意的是,这个函数名是我们要调用 JavaScript 的中转函数名,比如callFunctionReturnFlushedQueue。也就是说它的作用其实是处理参数,而非真正要调用的 JavaScript 函数。这个中转函数接收到的参数包含了ModuleId、MethodId和Arguments,然后由中转函数查找自己的模块配置表,找到真正要调用的 JavaScript 函数。在实际使用的时候,我们可以这样发起对 JavaScript 的调用:

31、在这个方法中,有一个很关键的方法:processMethodSignature,它会根据 JavaScript 的 CallbackId 创建一个 Block,并且在调用完函数后执行这个 Block。实战应用俗话说:“思而不学则神棍”,下面举一个例子来演示 Objective-C 是如何与 JavaScript 进行交互的。首先新建一个模块:

33、在 JavaScript 中,可以这样调用:

34、有兴趣的同学可以复制以上代码并自行调试。React 鲍伊酷雪Native 优缺点分析经过一长篇的讨论,其实 React Native 的优缺点已经不难分析了,这里简单缍那傺蒙总结一下:优点复用了 React 的思想,有利于前端开发者涉足移动端。能够利用 JavaScript 动态更新的特性,快速迭代。相比于原生平台,开发速度更快,相比于 Hybrid 框架,性能更好。缺点做不到Write once, Run everywhere,也就是说开发者依然需要为 iOS 和 Android 平台提供两套不同的代码,比如参考官方文档可以发现不少组件和API都区分了 Android 和 iOS 版本。即使是共用组件,也会有平台独享的函数。不能做到完全屏蔽 iOS 端或 Android 的细节,前端开发者必须对原生平台有所了解。加重了学习成本。对于移动端开发者来说,完全不具备用 React Native 开发的能力。由于 Objective-C 与 JavaScript 之间切换存在固定的时间开销,所以性能必定不及原生。比如目前的官方版本无法做到 UItableview(ListView) 的视图重用,因为滑动过程中,视图重用需要在异步线程中执行,速度太慢。这也就导致随着 Cell 数量的增加,占用的内存也线性增加。