共计 7031 个字符,预计需要花费 18 分钟才能阅读完成。
图片起源:https://unsplash.com/photos/g…
作者:五廿
跨端通信
在挪动端开发场景中,能应用一份代码就能同时在安卓和 iOS 零碎上运行 APP 的计划,熟称为跨端计划。而 Webview,React Native 都是云音乐大前端团队用的比拟多的跨端计划,这些计划尽管能进步开发效率,但它们不能像原生语言一样间接调用零碎的能力,于是在做 HTML5(以下简称 H5)或者 React Native(以下简称 RN)需要的时候,开发者们常常碰到要调用 Native 能力的状况。Native 能力用原生语言编写,有本人的运行环境,RN 页面应用 JS 编写,也有独立的运行环境,这种逾越运行环境的调用被称为 跨端通信
。
H5 中的 跨端通信
称为 JSBridge
,在进行一次 JSBridge 调用的时候会携带调用参数,默认有 4 个参数:
ModuleId: 模块 ID
MethodId: 办法 ID
params: 参数
CallbackId: JS 回调名
其中 ModuleId
和 MethodId
能定位到具体调用的原生办法,params
参数作为原生办法调用的参数,最初通过 CallbackId
回调 JS 的回调函数,H5 就能从回调函数中拿到调用后果。该流程中次要应用了 Webview 容器中 拦挡申请
和客户端调用 JS 函数
的能力,比方安卓中通常应用的是 WebChromeClient.onJsPrompt
办法来拦挡 H5 的申请,evaluateJavascript
办法用来执行回调。然而 React Native 中没有引入 Webview 来实现这些调用的能力,它采纳了齐全不同的形式来解决。另外,在云音乐团队的 APP 中,会同时存在 H5 和 RN 页面,也就是同一个 APP 中两种跨端通信形式并存,但它们最初调用的原生办法却是来自同一个原生模块。本文次要从 Android 零碎的 RN 实现来介绍 RN 的通信机制和桥接能力(以下简称 Bridge),并联合以上通信场景中会碰到的问题来解说如何实现一个业务中可用的 Bridge。大体由三局部组成,首先介绍 RN 中不同的组成模块和它们各自的角色;第二局部是各个模块之间的调用形式和具体的示例;最初一部分探讨业务中的 Bridge 的实现。
RN 组成
在 RN 中,次要有三个重要的组成模块:平台层
(Android 或者 OC 环境), 桥接层
(C++)和JS 层
。
- 平台层负责原生组件的渲染和提供各式各样的原生能力,由原生语言实现;
- 桥接模块负责解析 JS 代码,JS 和 Java/OC 代码互调,由 C++ 语言实现;
- JS 层负责跨端页面具体的业务逻辑。
相比起 Webview 的构造来说,RN 的构造多了一层 桥接层
,也就是 C++ 层。文章先来介绍一下这个模块的作用,以及为什么会多出这么一个模块。
桥接层(C++ 层)
React Native 和 H5 一样,应用了 JS 作为跨端页面的开发语言,因而它必须要有一个 JS 执行引擎,而在应用 H5 的状况下,Webview 是 JS 的执行引擎,同时 Webview 还是页面的渲染引擎。RN 不一样的中央在于,曾经有了本人的渲染层,这个性能交给了 Java 层,因为 RN 的 JS 组件代码最初都会渲染成原生组件。因而 RN 只须要一个 JS 执行引擎来跑 React 代码。RN 团队抉择了 JSCore
作为 JS 的执行引擎,而 JSCore
的对外接口是用 C 和 C++ 编写的。因而平台层的 Java 代码 / OC 代码想要通过 JSCore
拿到 JS 的模块和回调函数,只能通过 C++ 提供的接口获取,再加上 C++ 在 iOS 和安卓零碎上也有良好的跨端运行的性能,选它作为桥接层是不错的抉择。
JSCore
JSCore 是桥接层中的次要模块,它是 RN 架构中的 JS 引擎,负责 JS 代码的加载和解析。先来看下它的次要 API:
JSContextGetGlobalObject:获取 JavaScript 运行环境的 Global 对象。JSObjectSetProperty/JSObjectGetProperty:JavaScript 对象的属性操作:set 和 get。JSEvaluateScript:在 JavaScript 环境中执行一段 JS 脚本。JSObjectCallAsFunction:在 JavaScript 环境中调用一个 JavaScript 函数
通过 API 能够看进去,开发者能够用 JSEvaluateScript
在 JSCore 环境中执行一段 JS 代码,也能够通过 JSContextGetGlobalObject
拿到 JS 上下文的 Global 变量,而后把它转化成 C++ 能够应用的数据结构并且操作它,注入 API。而 JSObjectSetProperty
和 JSContextGetGlobalObject
也是比拟重要的两个 API,稍后会在通信流程中发挥作用。
Native 模块和 JavaScript 模块
说起通信的话,整个过程必定存在信源和信宿,也就是音讯的发送者和接收者,在 RN 的通信中,它们是 Native 和 JS 的模块,它们向对方提供能力都是以模块为性能单位的,相似 JSBridge 协定中的 ModuleID 的概念。
- Native 模块在 Android 零碎下是 Java 模块,由平台代码实现,JS 通过模块 ID(moduleID)和办法 ID(methodID)来进行调用,个别都在 RN 源码工程的
java/com/facebook/react/modules/
目录下,能够给 RN 页面凋谢原生零碎的能力,如计时器的实现模块Timing
,给 JS 代码提供计时器的能力。 - JavaScript 模块是由 JS 实现,代码在
/Libraries/ReactNative/
目录下,如 App 启动模块AppRegistery
,对 Java 环境来说,作用是提供操作 JS 环境的 API,如回调,播送等。Java 的调用办法是通过 JS 裸露进去的callFunctionReturnFlushedQueue
API。
JS 环境中会保护一份所有 Native 模块的 moduleID 和 methodID 的映射 NativeModules
,用来调用 Native 模块的时候查找对应 ID;Java 环境中也会保护一份 JavaScript 模块的映射 JSModuleRegistry
,用来调用 JS 代码。而在理论的代码中,Native 模块和 JS 模块的通信须要通过中间层也就是 C++ 层的过渡,也就是说 Native 模块和 JS 模块实际上都只是在和 C++ 模块进行通信。
C++ 和 JS 通信
下面提到,JSCore 能够让 C++ 拿到 JS 运行环境的 global 对象并能操作它的属性,而 JS 代码会在 global 对象中注入一些原生模块须要的 API,这是 JS 向 C++ 提供操作 API 的次要形式。
- RN 环境中 JS 会在 global 对象中设置了
__fbBatchedBridge
变量,并在变量塞入了 4 个的 API,作为 JS 被调用的入口,次要 API 包含:
<!—->
callFunctionReturnFlushedQueue // 让 C++ 调用 JS 模块
invokeCallbackAndReturnFlushedQueue // 让 C++ 调用 JS 回调
flushedQueue // 清空 JS 工作队列
callFunctionReturnResultAndFlushedQueue // 让 C++ 调用 JS 模块并返回后果
- JS 还在 global 中还设置了
__fbGenNativeModule
办法,用来给 C++ 调用后在 JS 环境生成 Java 模块的映射对象,也就是NativeModules
模块。它的数据结构相似于(跟理论的数据结构有偏差):
<!—->
{
"Timing": {
"moduleID": "1001",
"method": {
"createTimer": {"methodID": "10001"}
}
}
}
- 通过
NativeModules
的映射,开发者能拿到调用模块和办法的moduleID
和methodID
,在调用过程中会映射到具体的 Native 的办法。
同样的,C++ 通过 JSCore 的 JSObjectSetProperty
办法在 global 对象中塞入了几个 Native API,让 JS 能通过它们来调用 C++ 模块。次要 API 有:
nativeFlushQueueImmediate // 立刻清空 JS 工作队列
nativeCallSyncHook // 同步调用 Native 办法
nativeRequire // 加载 Native 模块
- 下面介绍 API 的时候,有多个 API 的性能比拟相似,就是清空 JS 的工作队列,那是因为 JS 在调用 Native 模块是异步调用,它会把调用参数包装成一个调用工作放入 JS 工作队列
MessageQueue
中,而后期待 Native 的调用。调用机会个别是在触发事件的时候,事件会触发 Native 回调 JS 的回调函数,Native 模块须要通过__fbBatchedBridge
的四个 API 回调 JS 代码,而这四个 API,都有flushedQueue
性能:清空工作队列并执行所有的工作,借此来生产队列中的 Native 调用工作。然而如果某一次调用间隔上一次的flushedQueue
行为有点久(个别是大于 5 ms),就会触发立刻调用的逻辑,JS 调用nativeFlushQueueImmediate
API,被动触发工作生产。
平台(Java)和 C++ 的通信
Java 跟 C++ 的相互调用通过 JNI(Java Native Interface),通过 JNI,C++ 层会裸露进去一些 API 来给 Java 层调用,来让 Java 能跟 JS 层进行通信。上面是 C++ 通过 JNI 裸露给 Java 的一些办法:
initializeBridge // 初始化:C++ 从 Java 拿到 Native 模块,作为参数传给 JS 生成 NativeModules
jniLoadScriptFromFile // 加载 JS 文件
jniCallJSFunction // 调用 JS 模块
jniCallJSCallback// 调用 JS 回调
setGlobalVariable // 编辑 global 变量
getJavaScriptContext // 获取 JS 运行环境
- 由下面的 API 根本能够判断出,C++ 负责的是一些中间层的角色,有 JS 的加载,解析的工作,还有提供操作 JS 运行环境的 API;
- 这里操作 JS 的 API 都会走到上一节
__fbBatchedBridge
的四个 API 上,如jniCallJSFunction
会调用callFunctionReturnFlushedQueue
。jniCallJSCallback
会调用invokeCallbackAndReturnFlushedQueue
。由此,三个模块的调用链路就连贯了起来。
调用示例
以 RN 中的 setTimeout 办法为例,走一遍调用流程。
- 初始化过程
- Timing Class:Native 中的延时调用的实现类,被 @reactModule 装璜器形容为一个 Native 模块,在 RN 初始化的时候被放入 ModuleRegistry 映射表,用于前面的调用映射。
- ModuleRegistry 映射表结构实现后,调用 C++ 的 initializeBridge,把 ModuleRegistry 的模块通过 \__fbGenNativeModule 函数注册进 JS 环境。
-
JS 代码中的 JSTimer 类 援用 Timing 模块的 createTimer 来实现 setTimeout,提早执行函数。
// 源代码地位:/Libraries/Core/Timers/JSTimers.js const {Timing} = require('../../BatchedBridge/NativeModules'); function setTimeout(func: Function, duration: number, ...args: any): number { // 创立回调函数 const id = _allocateCallback(() => func.apply(undefined, args), 'setTimeout', ); Timing.createTimer(id, duration || 0, Date.now(), /* recurring */ false); return id; },
- setTimeout 的调用过程
- 当 setTimeout 在 JSTimer.js 被调用,通过 NativeModules 找到 Timing Class 的 moduleID 和 methodID,放进工作队列
MessageQueue
中; - Native 通过事件或者被动触发清空
MessageQueue
队列,C++ 层把 moduleID,methodID 和其余调用参数交给ModuleRegistry
,由它来找到 Native 模块的代码,Timing 类; - Timing 调用
createTimer
办法,调用零碎计时性能实现提早调用; -
计时完结,Timing 类须要回调 JS 函数
// timerToCall 是回调函数的 ID 数组 getReactApplicationContext().getJSModule(JSTimers.class) .callTimers(timerToCall);
getJSModule
办法会通过JSModuleRegistry
找到须要调用的 JS 模块,并调用对应的办法,该流程中调用JSTimers
模块的callTimers
办法。- Java 代码通过 JNI 接口
jniCallJSFunction
通过 C++ 调用 JS 模块,并传入 module:JSTimers
和 method:callTimers
; - C++ 调用 JS 裸露进去的
callFunctionReturnFlushedQueue
API,带上 module 和 method,回到 JS 的调用环境; - JS 执行
callFunctionReturnFlushedQueue
办法找到 RN 初始化阶段注册好的 JSTimer 模块的callTimers
函数,进行调用。调用结束后清空一下工作队列MessageQueue
。
RN 的 JSBridge
以上通过 RN 的 setTimeout 函数走了一遍 RN 内 Java 代码和 JS 代码的通信流程。简略来说,Java 模块和 JS 模块能够通过 NativeModules 和 JS 回调函数相互调用,来达成一次跨端调用。然而业务中的 Bridge 须要蕴含一些额定的场景,比方并发调用,事件监听等。
- 并发调用:相似于在 web 端同时发多个申请,为了将申请后果回调到正确的回调函数内,须要保留一个申请到回调函数的映射,在 Bridge 的调用中也是一样的。而这份映射能够保护在 JS 代码中,也能够保护在 Native 代码中,在跨端计划中,两者都可行的状况下个别抉择 JS 代码的计划来放弃灵活性,Native 只负责处理结果并回调。
- 事件监听:比方 JS 代码监听页面是否切换到后盾,同一个回调函数在页面屡次切换到后盾的时候,应该要被调用屡次,然而 RN 的 JSCallback 只容许调用一次(每一个 callback 实例会带上是否调用过的标记),回调显然不适宜这种场景,云音乐的 Bridge 应用 RN 的事件告诉:
RCTDeviceEventEmitter
来代替回调。RCTDeviceEventEmitter
是一个纯 JS 实现的事件订阅散发模块,Native 模块通过getJSModule
能够拿到它的办法,因而能够在 Native 端收回一个 JS 事件并带上回调的参数和映射 ID 等,而不必走 JSCallback。
回到之前的问题:如何实现 RN 的 Bridge,能让一个 Bridge 的 API 同时反对 H5 和 RN 的调用。因为 H5 和 RN 大多数的业务场景都是雷同的,比方获取用户信息 user.info,设施信息 device.info 相似的接口,在 H5 和 RN 中都是会用到的。除了跨端调用的协定要保持一致外,具体的实现模块,协定解析模块都是能够复用的。其中不一样的就是调用链路。RN 链路中的次要模块包含:
- 给 JS 代码调用的 NativeModule,作为调用入口,JS 代码调用它裸露进去的办法传入调用参数并开始调用流程,然而该模块不解析协定和参数,能够称作
RNRPCNativeModule
; - 在 Native 模块解决完后,
RNRPCNativeModule
应用RCTDeviceEventEmitter
生成一个事件回调给 JS 代码,并带上执行后果。
除了以上两个不一样的模块外,其余模块都是能够复用的,如协定解析和工作散发模块,解析协定的调用模块,办法,参数等,并把它分发给具体的 Native 模块;还有 Native 具体的性能实现模块,都能够保持一致。
联合后面介绍的调用流程,开发者如果调用 User.info
这个 JSBridge 来获取用户信息,调用流程如下:
这样的解决,能保障 H5 和 RN 能用同一份 moduleID 和 methodID 来调用 Native 的性能,而且保障在同一个模块进行解决。从开发者的角度来看,就是一个 Bridge 的 API 能够同时反对 H5 和 RN 的调用。
以上。
相干材料
- React Native 源代码
- React Native 原生模块和 JS 模块交互
- Handler 与 Looper,MessageQueue 的关系
- React Native 通信机制详解
- React Native 源码解析
- How React Native constructs app layouts
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!