hyice / QAPool

收集一些平常遇到的问题,记录解决的过程和一些总结性思考
MIT License
2 stars 0 forks source link

JavaScript是如何调用 NativeModule 的方法的? #4

Open hyice opened 6 years ago

hyice commented 6 years ago

问题背景

React Native的开发过程中,经常需要在原生端桥接方法给JavaScript调用,比如在官方文档里的这个例子:

// CalendarManager.h
#import <React/RCTBridgeModule.h>

@interface CalendarManager : NSObject <RCTBridgeModule>
@end

// CalendarManager.m
@implementation CalendarManager

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
    RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}

@end
import {NativeModules} from 'react-native';
var CalendarManager = NativeModules.CalendarManager;
CalendarManager.addEvent('Birthday Party', '4 Privet Drive, Surrey');

那么这一套机制是怎么运作的呢?

考虑到整个流程涉及到了两端多处代码,为了逻辑清晰,我们就从使用者的角度来跟一下整个流程,看看各个部分都是怎么实现的。

桥接原生模块

RCT_EXPORT_MODULE

从示例代码里可以发现,Native Module 是通过RCT_EXPORT_MODULE这个宏完成桥接的,所以,我们首先来看看这个宏做了哪些事。

#define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

从宏定义中我们可以看到,这个宏实现了两个类方法,一个是+moduleName,一个是+load。前者先不管,我们先来看看+load方法。我们知道,load 方法会在类加载的时候被触发一次,然后RCTRegisterModule方法就会被调用。

void RCTRegisterModule(Class moduleClass)
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
      RCTModuleClasses = [NSMutableArray new];
  });

  RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
            @"%@ does not conform to the RCTBridgeModule protocol",
            moduleClass);

  // Register module
  [RCTModuleClasses addObject:moduleClass];
}

在这个方法中,首先会在全局第一次调用时生成一个可变数组RCTModuleClasses,用于保存所有需要桥接到JavaScript端的模块。

注意:在真正进行保存操作前,还会校验该模块是否满足RCTBridgeModule协议。所以,记得加上协议支持哦。至于协议里的约束,除了+moduleName是必须实现的,其他都是可选的。因此,协议里的其他内容我们暂时不涉及。

ModuleData

现在,虽然我们需要桥接的模块已经成功加入了一个数组里,但他们还没有真正完成桥接,我们还需要看看这个数组是在什么时候以及如何使用的。

Bridge的初始化过程中,会触发下面这个调用,其中RCTGetModuleClasses方法获取到的就是刚才那个可变数组。

(void)[self _initializeModules:RCTGetModuleClasses() withDispatchGroup:prepareBridge lazilyDiscovered:NO];

继续往下跟这个方法,我们会发现每一个需要桥接的模块类,都会生成一个对应的RCTModuleData实例。关于RCTModuleData的具体结构,我们后面再细讲。

-[RCTCxxBridge _initializeModules:withDispatchGroup:lazilyDiscovered:]
    -[RCTCxxBridge registerModulesForClasses:]
        -[RCTModuleData initWithModuleClass:bridge:]
ModuleRegistry

而所有生成的RCTModuleData实例又会在后续的处理逻辑里,用于生成ModuleRegistry对象。

-[RCTCxxBridge _initializeModules:withDispatchGroup:lazilyDiscovered:]
    -[RCTCxxBridge _initializeBridge]
        -[RCTCxxBridge _buildModuleRegistry]
        Instance::initializeBridge

ModuleRegistry对象初始化的时候接收了两个参数,一个是没有查找到 Module 的回调函数,另一个则是由NativeModule组成的一个数组。而NativeModule只是一个抽象类,有两个子类RCTNativeModuleCxxNativeModule,都是基于上面生成的RCTModuleData进行了一层封装,提供抽象类中所定义的这些功能:

class NativeModule {
 public:
  virtual ~NativeModule() {}
  virtual std::string getName() = 0;
  virtual std::vector<MethodDescriptor> getMethods() = 0;
  virtual folly::dynamic getConstants() = 0;
  virtual void invoke(unsigned int reactMethodId, folly::dynamic&& params, int callId) = 0;
  virtual MethodCallResult callSerializableNativeHook(unsigned int reactMethodId, folly::dynamic&& args) = 0;
};

生成出来的ModuleRegistry实例会被作为参数传给Instance::initializeBridge方法,接着在 Instance 的初始化方法中,ModuleRegistry实例会被赋值给 Instance 的成员变量。

使用原生模块

nativeModuleProxy

现在,我们已经生成了一份包含所有原生模块信息的ModuleRegistry,但我们需要先把它放一放,把目光转到JavaScript端看看Native Module是如何使用的。我们稍候再回头来看ModuleRegistry是在什么时候使用的。

我们在JavaScript端是这么使用我们桥接的模块的:

import {NativeModules} from 'react-native';
var CalendarManager = NativeModules.CalendarManager;

通过上面代码可以发现,我们实际桥接的模块都是NativeModules身上的属性,那么NativeModules又是什么呢?我们来看一下NativeModules.js:

let NativeModules : {[moduleName: string]: Object} = {};
if (global.nativeModuleProxy) {
    NativeModules = global.nativeModuleProxy;
} else {
    ...
}

module.exports = NativeModules;

所以,我们访问的原生模块实际上是nativeModuleProxy这个 global 对象上的属性。那么nativeModuleProxy又是什么呢?我们可以简单地通过搜索找到这个对象的注入代码:

// Instance::initializeBridge
//    NativeToJsBridge::NativeToJsBridge
//        JSCExecutorFactory::createJSExecutor
//            JSCExecutor::JSCExecutor

installGlobalProxy(
    m_context,
    "nativeModuleProxy",
    exceptionWrapMethod<&JSCExecutor::getNativeModule>());

在刚才保存ModuleRegistry实例的Instance::initializeBridge方法中,同时会生成一个NativeToJsBridge的实例,而在这个实例的初始化方法中,又会通过传入的JSExecutorFactory参数生成一个JSExecutor实例。在JSExecutor的构造函数中,往JavaScript的执行 context 中注入了一个 global 变量nativeModuleProxy,传入的getNativeModule方法会在试图访问nativeModuleProxy的属性时被触发。

现在,我们再来深入看看getNativeModule这个函数的实现,我们跳过中间的一些逻辑判断,直接跟着调用栈到核心处理逻辑:

JSCExecutor::getNativeModule
    JSCNativeModules::getModule
        JSCNativeModules::createModule
            ModuleRegistry::getConfig
            [JavaScript]global.__fbGenNativeModule

这里有两个重点,一个是如何根据之前的ModuleRegistry生成对应模块的配置ModuleConfig,另一个是如何根据这份配置信息生成JavaScript端使用的对象。

ModuleRegistry::getConfig

终于,前面准备了这么久的ModuleRegistry要被用到了。根据前面的分析,我们知道,ModuleRegistry的核心还是最开始转换出来的RCTModuleData,而这里getConfig方法最终获取到的数据也基本来自RCTModuleData

这里就不列出源码了,有点长,我用伪代码描述一下:

folly::Optional<ModuleConfig> ModuleRegistry::getConfig(const std::string& name) {
    NativeModule *module = FindModuleByName(name);

    if (NotFound) {
        ModuleNotFoundCallback();
        return nullptr;
    }

    GenerateConfigArray(
        name,
        moduleConstants, // RCTModuleData exportedConstants
        moduleMethodNames, // RCTModuleData methods
        modulePromiseMethodIds, // 根据 method 的 functionType 分类
        moduleSyncMethodIds
    );

    if (NoUsefulModuleData) {
        return nullptr;
    } else {
        return ModuleConfig{moduleIndex, moduleConfig};
    }
}
__fbGenNativeModule

生成好的ModuleConfig会传给JavaScript端的__fbGenNativeModule函数,我抽取了一下核心的逻辑:

folly::Optional<Object> JSCNativeModules::createModule(const std::string& name, JSContextRef context) {
    m_genNativeModuleJS = global.getProperty("__fbGenNativeModule").asObject();
    auto result = m_moduleRegistry->getConfig(name);
    Value moduleInfo = m_genNativeModuleJS->callAsFunction({
        Value::fromDynamic(context, result->config),
        Value::makeNumber(context, result->index)
    });

    folly::Optional<Object> module(moduleInfo.asObject().getProperty("module").asObject());

    return module;
}

所以,一切最后又回到了JavaScript端的__fbGenNativeModule函数。

global.__fbGenNativeModule = genModule;
function genModule(
  config: ?ModuleConfig,
  moduleID: number,
): ?{name: string, module?: Object} {

  const [moduleName, constants, methods, promiseMethods, syncMethods] = config;

  const module = {};
  methods &&
    methods.forEach((methodName, methodID) => {
      const isPromise =
        promiseMethods && arrayContains(promiseMethods, methodID);
      const isSync = syncMethods && arrayContains(syncMethods, methodID);
      const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async';
      module[methodName] = genMethod(moduleID, methodID, methodType);
    });
  Object.assign(module, constants);

  return {name: moduleName, module};
}

调用桥接的方法

enqueueNativeCall

我们看到,最后所用到的 module 对象只是简单地把传入的方法和常量设置成了对应的属性,唯一的问题只剩下中间用到的genMethod方法了。genMethod方法会根据方法类型生成对应的方法,我们以async类型的方法为例,来看看生成的方法:

fn = function(...args: Array<any>) {
    ...

    BatchedBridge.enqueueNativeCall(
        moduleID,
        methodID,
        ...
    );
};

所以,当通过桥接的NativeModule调用桥接的方法时,实际触发的是BatchedBridge.enqueueNativeCall,并传入了moduleIDmethodID以及其余暂时被我们忽略的参数。

BatchedBridgeMessageQueue的实例,所以,我们来看一下MessageQueue上的enqueueNativeCall方法,去除所有参数相关的操作后,核心逻辑是这样的:

enqueueNativeCall(
    moduleID: number,
    methodID: number,
    params: any[],
    onFail: ?Function,
    onSucc: ?Function,
) {
    this._queue[MODULE_IDS].push(moduleID);
    this._queue[METHOD_IDS].push(methodID);
    this._queue[PARAMS].push(params);

    const now = new Date().getTime();
    if (
        global.nativeFlushQueueImmediate &&
        now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
    ) {
        const queue = this._queue;
        this._queue = [[], [], [], this._callID];
        this._lastFlush = now;
        global.nativeFlushQueueImmediate(queue);
    }
}

这个方法依次把moduleIDmethodIdparams加入了消息队列,然后将队列丢给global.nativeFlushQueueImmediate去执行队列里的任务。

nativeFlushQueueImmediate

执行队列的函数是native注入到JavaScript的,因此,任务又回到了原生端。

installNativeHook<&JSCExecutor::nativeFlushQueueImmediate>(
      "nativeFlushQueueImmediate");

跟进installNativeHook里面去,我们会发现设置给JavaScript端的方法并不是nativeFlushQueueImmediate,而是在这个方法外面还包了一层,处理了异常情况,并将参数数量和具体参数传给这个方法。而nativeFlushQueueImmediate则是做了一下参数校验,确保只有queue这唯一一个参数。接下来,在真正进行方法调用前,还有以下几级调用:

JSCExecutor::nativeFlushQueueImmediate
    JSCExecutor::flushQueueImmediate
        JsToNativeBridge::callNativeModules
            ModuleRegistry::callNativeMethod

我们可以看到,逻辑又回到了我们之前生成的ModuleRegistry,在这里面会根据moduleID找到对应的NativeModule,并调用它的invoke方法,我们以RCTNativeModule为例看看方法接下来的调用路径:

ModuleRegistry::callNativeMethod
    RCTNativeModule::invoke
        static invokeInner
            -[RCTModuleMethod invokeWithBridge:module:arguments:]

在这个方法里,首先会根据方法的特征生成一个NSInvocation实例,然后会把传入的参数根据一定的规则塞入NSInvocation中去,最后则会调用:

[_invocation invokeWithTarget:module];

这样,我们桥接的模块上的方法就成功被调用了。

未完待续

不过,我们其实还有很多问题没有解决,比如:

你可以按照上文的流程试着自己阅读源码去寻找这些问题的答案。当然,我之后的文章也有可能涉及这些问题哦。