在刚才保存ModuleRegistry实例的Instance::initializeBridge方法中,同时会生成一个NativeToJsBridge的实例,而在这个实例的初始化方法中,又会通过传入的JSExecutorFactory参数生成一个JSExecutor实例。在JSExecutor的构造函数中,往JavaScript的执行 context 中注入了一个 global 变量nativeModuleProxy,传入的getNativeModule方法会在试图访问nativeModuleProxy的属性时被触发。
问题背景
在
React Native
的开发过程中,经常需要在原生端桥接方法给JavaScript
调用,比如在官方文档里的这个例子:那么这一套机制是怎么运作的呢?
考虑到整个流程涉及到了两端多处代码,为了逻辑清晰,我们就从使用者的角度来跟一下整个流程,看看各个部分都是怎么实现的。
桥接原生模块
RCT_EXPORT_MODULE
从示例代码里可以发现,Native Module 是通过
RCT_EXPORT_MODULE
这个宏完成桥接的,所以,我们首先来看看这个宏做了哪些事。从宏定义中我们可以看到,这个宏实现了两个类方法,一个是
+moduleName
,一个是+load
。前者先不管,我们先来看看+load
方法。我们知道,load 方法会在类加载的时候被触发一次,然后RCTRegisterModule
方法就会被调用。在这个方法中,首先会在全局第一次调用时生成一个可变数组
RCTModuleClasses
,用于保存所有需要桥接到JavaScript
端的模块。注意:在真正进行保存操作前,还会校验该模块是否满足
RCTBridgeModule
协议。所以,记得加上协议支持哦。至于协议里的约束,除了+moduleName
是必须实现的,其他都是可选的。因此,协议里的其他内容我们暂时不涉及。ModuleData
现在,虽然我们需要桥接的模块已经成功加入了一个数组里,但他们还没有真正完成桥接,我们还需要看看这个数组是在什么时候以及如何使用的。
在
Bridge
的初始化过程中,会触发下面这个调用,其中RCTGetModuleClasses
方法获取到的就是刚才那个可变数组。继续往下跟这个方法,我们会发现每一个需要桥接的模块类,都会生成一个对应的
RCTModuleData
实例。关于RCTModuleData
的具体结构,我们后面再细讲。ModuleRegistry
而所有生成的
RCTModuleData
实例又会在后续的处理逻辑里,用于生成ModuleRegistry
对象。ModuleRegistry
对象初始化的时候接收了两个参数,一个是没有查找到 Module 的回调函数,另一个则是由NativeModule
组成的一个数组。而NativeModule
只是一个抽象类,有两个子类RCTNativeModule
和CxxNativeModule
,都是基于上面生成的RCTModuleData
进行了一层封装,提供抽象类中所定义的这些功能:生成出来的
ModuleRegistry
实例会被作为参数传给Instance::initializeBridge
方法,接着在 Instance 的初始化方法中,ModuleRegistry
实例会被赋值给 Instance 的成员变量。使用原生模块
nativeModuleProxy
现在,我们已经生成了一份包含所有原生模块信息的
ModuleRegistry
,但我们需要先把它放一放,把目光转到JavaScript
端看看Native Module
是如何使用的。我们稍候再回头来看ModuleRegistry
是在什么时候使用的。我们在
JavaScript
端是这么使用我们桥接的模块的:通过上面代码可以发现,我们实际桥接的模块都是
NativeModules
身上的属性,那么NativeModules
又是什么呢?我们来看一下NativeModules.js
:所以,我们访问的原生模块实际上是
nativeModuleProxy
这个 global 对象上的属性。那么nativeModuleProxy
又是什么呢?我们可以简单地通过搜索找到这个对象的注入代码:在刚才保存
ModuleRegistry
实例的Instance::initializeBridge
方法中,同时会生成一个NativeToJsBridge
的实例,而在这个实例的初始化方法中,又会通过传入的JSExecutorFactory
参数生成一个JSExecutor
实例。在JSExecutor
的构造函数中,往JavaScript
的执行 context 中注入了一个 global 变量nativeModuleProxy
,传入的getNativeModule
方法会在试图访问nativeModuleProxy
的属性时被触发。现在,我们再来深入看看
getNativeModule
这个函数的实现,我们跳过中间的一些逻辑判断,直接跟着调用栈到核心处理逻辑:这里有两个重点,一个是如何根据之前的
ModuleRegistry
生成对应模块的配置ModuleConfig
,另一个是如何根据这份配置信息生成JavaScript
端使用的对象。ModuleRegistry::getConfig
终于,前面准备了这么久的
ModuleRegistry
要被用到了。根据前面的分析,我们知道,ModuleRegistry
的核心还是最开始转换出来的RCTModuleData
,而这里getConfig
方法最终获取到的数据也基本来自RCTModuleData
。这里就不列出源码了,有点长,我用伪代码描述一下:
__fbGenNativeModule
生成好的
ModuleConfig
会传给JavaScript
端的__fbGenNativeModule
函数,我抽取了一下核心的逻辑:所以,一切最后又回到了
JavaScript
端的__fbGenNativeModule
函数。调用桥接的方法
enqueueNativeCall
我们看到,最后所用到的 module 对象只是简单地把传入的方法和常量设置成了对应的属性,唯一的问题只剩下中间用到的
genMethod
方法了。genMethod
方法会根据方法类型生成对应的方法,我们以async
类型的方法为例,来看看生成的方法:所以,当通过桥接的
NativeModule
调用桥接的方法时,实际触发的是BatchedBridge.enqueueNativeCall
,并传入了moduleID
,methodID
以及其余暂时被我们忽略的参数。BatchedBridge
是MessageQueue
的实例,所以,我们来看一下MessageQueue
上的enqueueNativeCall
方法,去除所有参数相关的操作后,核心逻辑是这样的:这个方法依次把
moduleID
,methodId
和params
加入了消息队列,然后将队列丢给global.nativeFlushQueueImmediate
去执行队列里的任务。nativeFlushQueueImmediate
执行队列的函数是
native
注入到JavaScript
的,因此,任务又回到了原生端。跟进
installNativeHook
里面去,我们会发现设置给JavaScript
端的方法并不是nativeFlushQueueImmediate
,而是在这个方法外面还包了一层,处理了异常情况,并将参数数量和具体参数传给这个方法。而nativeFlushQueueImmediate
则是做了一下参数校验,确保只有queue
这唯一一个参数。接下来,在真正进行方法调用前,还有以下几级调用:我们可以看到,逻辑又回到了我们之前生成的
ModuleRegistry
,在这里面会根据moduleID
找到对应的NativeModule
,并调用它的invoke
方法,我们以RCTNativeModule
为例看看方法接下来的调用路径:在这个方法里,首先会根据方法的特征生成一个
NSInvocation
实例,然后会把传入的参数根据一定的规则塞入NSInvocation
中去,最后则会调用:这样,我们桥接的模块上的方法就成功被调用了。
未完待续
不过,我们其实还有很多问题没有解决,比如:
你可以按照上文的流程试着自己阅读源码去寻找这些问题的答案。当然,我之后的文章也有可能涉及这些问题哦。