最新消息:欢迎访问若有所思,如果你也有与我相同的兴趣爱好,请关注我的站点。

React Native通信原理解析(IOS)

技术专题 若远 20440浏览 0评论
||||| 3 |||||

引言

React-Native能够编写类HTML方式来编写客户端界面,并且能够动态变更视图内容,实现客户端界面的所见即所得,这无疑是非常让业界十分兴奋的一件事情。那么,React-Native是怎么实现的?它们之间的通信机制是怎么样的?

本文将会带来大家一探究竟,为各位一一答疑解惑。

基本原理

首先,我们来看一下在iOS中Native如何调用JS。从iOS7开始,系统进一步开放了WebCore SDK,提供JavaScript引擎库,使得我们能够直接与引擎交互拥有更多的控制权。其中,有两个最基础的概念:

通过这两个类,我们能够非常方便的实现Javascript与Native代码之间的交互,首先我们通过一个简单示例来观察Native如何调用Javascript代码:

那么,JSContext如何访问我们本地客户端OC代码呢?答案是通过Blocks和JSExports协议两种方式。
我们来看一个通过Blocks来实现JS访问本地代码的示例:

关于JSCore库的更多学习介绍,请看JavaScriptCore

React Native初始化过程解析

在了解React-Native中JS->Native的具体调用之前,我们先做一些准备工作,看看框架中Native app的启动过程。打开FB提供的AwesomeProject定位到appDelegate的didFinishLaunchingWithOptions方法中:

可以看到使用集成非常简单,那么RCTRootView到底做了哪些事情最后渲染将视图呈现在用户面前呢?
我们继续跟着代码往下分析就会看到我们今天的主角RCTBridge。

RCTBridge是Naitive端的bridge,起着桥接两端的作用 。事实上具体的实现放置在RCTBatchedBridge中,在它的start方法中执行了一系列重要的初始化工作。这部分也是ReactNative SDK的精髓所在,基于GCD实现一套异步初始化组件框架。大致的工作流程如下图所示:

React原理流程图

1.Load JS Source Code(并行)

加载页面源码阶段。该阶段主要负责从指定的位置(网络或者本地)加载React Native页面代码。与initModules各模块初始化过程并行执行,通过GCD分组队列保证两个阶段完成后才会加载解析页面源码。

2.Init Module(同步)

初始化加载React Native模块。该阶段会将所有注册的Native模块类整理保存到一个以Module Id为下标的数组对象中(同时还会保存一个以Module Name为Key的Dictionary,用于做索引方便后续的模块查找)。
整个模块的基础初始化和注册过程在系统Load Class阶段就会完成。React Native对模块注册的实现还是比较巧妙、方便,只需要对目标类添加相应的宏即可。

  1. 注册模块。实现RCTBridgeModule协议,并且在响应的Implemention文件中添加RCT_EXPORT_MODULE宏,该宏会为所在类自动添加一个+load方法,调用RCTBridge的RCTRegisterModule实现在Load Class阶段就完成模块注册工作。
  2. 注册函数。待注册函数所在的类必须是已注册模块,在需要注册的函数前添加RCT_EXPORT_MODULE宏即可。

当然这里需要注意的问题时模块初始化是一个同步任务,它必须被同步加载,所以当模块较多时势必会带来高延迟的问题,也是在新的版本中SDK将Module Method改为Lazy Load的原因之一。

3.Setup JS Executor(并行)

初始化JS引擎。React Native在0.18中已经很好的抽象了原来了JSExecutor,目前实现了RCTWebSocketExecutor和RCTJSCExecutor两个脚本引擎的封装,前者用于通过WebSocket链接到Chrome调试,后者则是内置默认引擎直接通过IOS SDK JSContext来实现相关的逻辑。
另外,在本阶段还会通过block hook的方式注册部分核心API

  1. nativeRequireModuleConfig:用于在JS端获取对应的Native Module,在0.14后的版本React Native已经对初始化模块做了部分优化,把关于Native Module Method部分的加载工作放置在requireModuleConfig时才做
  2. nativeLoggingHook:调用Native写入日志
  3. nativeFlushQueueImmediate:手动触发执行当前Native Call队列中所有的Native处理请求
  4. nativePerformanceNow:用于性能统计,获取当前Native的绝对时间(毫秒)

对于模块类中想要声明的方法,需要添加RCT_EXPORT_METHOD宏。它会给方法名添加” rct_export “前缀。

4.Module Config(并行)

这步将第2步中的Native模块类转换成Json,保存为remoteModuleConfig。注意在这里获取到的列表并非含有完整模块信息,而仅仅是一个Module List而已。

5.Inject Json Configration To JS(并行,需要等待3、4完成)

第3、4步被加入到一个setupJSExecutorAndModuleConfig的dispatchGroup中,当这两步都完成后,触发该步。此时将第4步中生成的config注入到JS中,保存到名为__fbBatchedBridgeConfig 的js全局变量中。
真正补全模块信息是当JS页面源码中,调用nativeRequireModuleConfig这个API使用指定模块(当然这也是非常符合Require按需加载的理念)时补全模块信息。

6.Execute JS Source Code

当initModulesAndLoadSource监测到1、5执行完成后,触发该步。可以关注这里SDK通过GCD Group来控制并行初始化过程的先后关系。

接下来,系统会通过JSCore执行在第一阶段Load到的JS页面源码 。加载过程中在JS端发生了什么事情?我们在下一部分详细解释。

JS Source Code代码分析

JS的主入口index.ios.js在我们看来只有短短数十行,然而这不是最终执行的代码。React-Native页面源码需要通过Transform Server转换处理,并把转化后的模块一起合并为一个bundle.js,这个过程称为buildBundle。转换后的index.ios.bundle才是最终可被Javascript引擎直接解释运行的代码。下面我们按照主程序的逻辑来分析源码几个核心模块实现原理。
在React Server中需要查看Bundle的模块映射关系可以直接访问:http://localhost:8081/index.ios.bundle.map,查看相关依赖和Bundle的缓存则可以访问: http://localhost:8081/debug

BatchedBridge

在上一部分我们知道,Native完成模块初始化后会通过Inject Json Config将配置信息同步至JS里中的全局变量__fbBatchedBridgeConfig,打开BatchedBridge.js我们可以看到如下代码。

对于这段代码,我们可以得出以下几个结论:

  1. 在JS端也存在一个bridge模块BatchedBridge,也是与Native建立双向通信的关键所在
  2. BatchedBridge是一个MessageQueue实例,它在创建时传入了__fbBatchedBridgeConfig值保存Native端支持的模块列表配置

BatchedBridge在创建时将自己写入全局变量__fbBatchedBridge上,这样Native可以通过JSContext[@”__fbBatchedBridge”]访问到JS bridge对象。

MessageQueue

接着我们继续看MessageQueue,它在整个通讯链路的机制上面有着重要作用,首先我们来观察一下它的构造函数。

从构造函数,我们大致能了解MessageQueue的几个信息:

  1. RemoteModules属性,用于保存Native端模块配置
  2. Callbacks属性缓存js的回调方法
  3. Queue事件队列用于处理各类事件等

在构造函数中,解析Native传入的remoteModules JSON,转换成JS对象

Config Modules

根据上一步MessageQueue的逻辑,继续往下跟踪_genModules函数,可以看到在MessageQueue已经对Native注入的Module Config做了一次预处理,如果debug模式可以看到大致的数据结构会转换成如下表中所示结构(其中HTSimepleAPI是一个自定义模块)。

至于这样的预处理有什么作用,我们继续往下分析,后面再来总结。

Lazily Config Methods

对于NativeModule,它们在上一步之后只有一个包含Module Name等简单信息的Module List的对象,只有在实际调用了该模块之后才会加载该模块的具体信息(比如暴露的API等)。

这段代码定义了一个全局模块NativeModules,遍历之前取到的remoteModules,将每一个module在NativeModules对象上扩展了一个getter方法,该方法中通过nativeRequireModuleConfig进一步加载模块的详细信息,通过processModuleConfig对模块信息进行预处理。进一步分析代码就可以发现这个方法其实是Native中定义的全局JS Block(nativeRequireModuleConfig)。

接下来我们继续看processModuleConfig中具体的代码逻辑,如下表所示:

processModuleConfig方法的主要工作是生成methods配置,并对每一个method封装了一个闭包fn,当调用method时,会转换成成调用self.__nativeCall(moduleID, methodID, args, onFail, onSucc)方法

预处理完成后,在JavaScript环境中的Moudle Config信息才算完整,包含Module Name、Native Method等信息,具体信息如下所示。

还记得第二部分第5步中Native端生成的模块配置表吗?结合它的结构,我们可以得知:对于Module&Method,在Native和JS端都以数组的形式存放,数组下标即为它们的ModuleID和MethodID。

__nativeCall

分析完Bridge部分的映射关系以及模块加载,那么我们再来看看最终调用Native代码是如何实现的。当JS调用module.method时,其实调用了self.__nativeCall(module, method, args, onFail, onSucc),对于__nativeCall方法:

这段代码为每个method创建了一个闭包fn,在__nativeCall方法中,并且在这里做了两件重要的工作:

  1. 把onFail和onSucc缓存到_callbacks中,同时把callbackID添加到params
  2. 把moduleID, methodID, params放入队列中,回调Native代码.
    __nativeCall如何做到回调Native代码呢?看第二部分第3步,在初始化JS引擎JSExecutor Setup时,Native端注册一个全局block回调nativeFlushedQueueImmediate,nativeCall在处理完毕后,通过该回调把队列作为返回值传给Native。nativeFlushedQueueImmediate的实现如下所示。

这里的handleBuffer就是Native端解析JS的模块调用最后通过NSInvocation机制调用Native代码对应的逻辑。有兴趣的朋友继续跟踪handleBuffer代码会发现,他的实现和React在JS端定义的MessageQueue有惊人的相似之处。

Call JS function & Callbacks

最后,我们回过头来看看Native端是如何调用JS端的相关逻辑的,这部分我们需要回到MessageQueue.js代码中来,可以看到MessageQueue暴露了3个核心方法:’invokeCallbackAndReturnFlushedQueue’、’callFunctionReturnFlushedQueue’、’flushedQueue’。

callFunctionReturnFlushedQueue用于实现Native调用带有返回值的JS端函数(这里的返回值也是通过Queue来模拟);
invokeCallbackAndReturnFlushedQueue用于实现Native调用带有Call的JS端函数(可以将Native的Callback作为JS端函数的入参,JS端执行完后调用Native的Callback)。

对于callFunctionReturnFlushedQueue方法,它最终调用的是__callFunction:

可以看到,此处会根据Native传入的module, method,调用JS端相应的模块并传入参数列表args.
同时我们又可以获得对于MessageQueue的另一条推测,_callableModules用来存放JS端暴露给Native的模块,进一步分析我们可以发现SDK中正是通过registerCallableModules方法注册JS端暴露API模块。

对于JS bridge提供的调用回调方法invokeCallbackAndReturnFlushedQueue,原理上和callFunction差不多,不再细说。

Native->JS

综上所述,在JS端提供callFunctionReturnFlushedQueue,Native bridge调用JS端方法时,应该使用这个方法。查看Native代码实现可知,RCTBridge封装了enqueueJSCall方法调用JS,梳理Native->JS的整体交互流程如下图所示。

NativeToJs流程图

之前已经论述过,如果在NATIVE端需要自定义模块提供给JS端使用那么该类需要实现RCTBridgeModule协议 。
此外,React-Native提供了另一种基于通知的方式,通过RCTEventDispatcher发送消息通知 。eventDispatcher作为Native Bridge的属性,封装了sendEventWithName:body:方法。使用时,Native中类同样需要实现RCTBridgeModule协议,通过self.bridge发送通知,JS端对应事件的EventEmitter添加监听处理调用。
查看sendEvent方法的代码可以发现,这种方式本质上还是调用enqueueJSCall方法。官方推荐我们使用通知的方式来实现 Native->JS,这样可以减少模块初始化加载解析的时间。

JS->Native

最后,我们来看一下JS如何调用Native。答案是JS不会主动传递数据给Native,也不能直接调用Native(一种情况除外,在入口直接通过NativeModules调用API),只有在Native调用JS时才会通过返回值触发调用。因为Native是基于事件响应机制的,比如触摸事件、启动事件、定时器事件、回调事件等。
当事件发生时,Native会调用JS相应模块处理,完毕后再通过返回值把队列传递给Native执行对应的代码。

JsToNative流程图

如上图所示,整个调用过程可以归纳为:

  1. JS把需要Module, Method, args(CallbackID)保存在队列中, 作为返回值通过blocks回调Native
  2. Native调用相应模块方法,完成
  3. Native通过CallbackID调用JS回调

总结

React Native的通讯基础建立在传统的JS Bridge之上,不过对于Bridge处理的MessageQueue机制、模块定义、加载机制上的巧妙处理指的借鉴。对于上述的整个原理解析可以概括为以下四个部分:

  1. 在启动阶段,初始化JS引擎,生成Native端模块配置表存于两端,其中模块配置是同步取得,而各模块的方法配置在该方法被真正调用时懒加载。
  2. Native和JS端分别有一个bridge,发生调用时,调用端bridge查找模块配置表将调用转换成{moduleID, methodID, args(callbackID)},处理端通过同一份模块配置表转换为实际的方法实现。
  3. Native->JS,原理上使用JSCore从Native执行JS代码,React-Native在此基础上给我们提供了通知发送的执行方式。
  4. JS->Native,原理上JS并不主动调用Native,而是把方法和参数(回调)缓存到队列中,在Native事件触发并访问JS后,通过blocks回调Native。

以上就是React Native通讯原理的解析综述,如果对文章中分析内容有任何问题,欢迎指正、交流,如果觉得哪些地方介绍的不够深入也欢迎留言,也许我们可以进一步探讨更有意思的话题。

转载请注明:若有所思-胡磊 » React Native通信原理解析(IOS)

Guest若远Guest
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

网友最新评论 (3)

  1. 我有android版的。(^o^)/~
    yoyo3年前 (2016-05-26)回复
    • 嗯,可以编辑一个专题
      若远3年前 (2016-05-26)回复
  2. I love reading these articles because they're short but infarmotive.
    Amberly2年前 (2017-06-08)回复