从 react-native 的 js 和 native 通讯看看 JSI 是什么

react-native Nov 17, 2020

本文提纲:

  1. 什么是 JSI
  2. 在 v8 中注入方法和变量
    1.v8 运行 js 代码步骤
    2.向 js 中注入方法 3.向 js 中注入变量
  3. 从 React-native 源码看 js 和 native 的通讯
    1.js 到 native 的通讯
    2.native 到 js 的通信
  4. 简述 JSI 的实现

本文强烈建议打开react-native 源码对照着看,因为很多地方的代码我没有贴全,并且由于仓库更新频繁,本文写于 2020-11-17,react-native 版本为 v0.63.3

什么是 JSI

JSI 普遍翻译成 javascript interface,其作用是在 js 引擎(例如 v8)和 native 之间建立一层适配层,有了 JSI 这一层在 react-native 中提到了两个提升:

  • 1.可以更换引擎,react-native 默认的 js 引擎是 JSC,可以方便的更换成为 V8,或者 hermes(facebook 自研的 js 引擎),甚至 jerry-script 等。
  • 2.在 javascript 中可以直接引用并调用 C++注入到 js 引擎中的方法,这使得 native 和 js 层能够“相互感知”,不再像以前需要将数据 JSON 化,然后通过 bridge 在 js 和 native 之间传递。

1中的 improvement 很好理解,2中的内容更深层的解释是:react-native 在以前的架构中,如下图
jsi1
是通过中间层 bridge 进行通讯,当在 js 中需要调用 native 层的方法的时候,需要将消息做 json 序列化,然后发送给 native。由于数据是异步发送,可能会导致阻塞以及一些优化的问题(正如我们 js 异步中的 microtask 和 macrotask),与此同时因为 native 和 js 层无法相互感知(js 中没有对 native 的引用),当我们需要从 js 侧调用 native 的方法(比方说蓝牙)之前,需要先将蓝牙模块初始化,即使你可能在你的整个 app 中并没有用到这个模块。新的架构允许对于原生模块的按需加载,即需要的时候再加载, 并且在 js 中能够有对于该模块的引用, 意味着不需要通过 JSON 通讯了,这大大提高了启动的效率。
现在 react-native 的新架构如下:左下侧的 Fabric 是原生渲染模块,右侧的 turbo modules 是原生的方法模块,可以看出现在 JSI 连接这 native 和 JS 两层。
jsi2
简单画一下 jsi 和 js 引擎的关系如下:
jsi3

在 V8 中注入方法和变量

大家都知道的是有一些方法比如说console.log,setInterval,setTimeout等方法实际上是浏览器(chrome)或者 node 为我们注入的方法,js 引擎本身是没有这些方法的,也就是说很多方法都是在 js 引擎外侧注入的。那么我们有必要先了解一下如何 v8 中注入方法和变量:

  • 首先编译 V8 生成静态/动态库,在你的 C++文件中引入该库,具体操作请看这里,这是 v8 的官方教程,会指导你从编译 v8 开始,到运行一个可以输出“Hello world”的 js 代码片段,有点像是在 c++中执行eval("'Hello ' + 'World'")
  • 经过上一步骤我们简单得出如何通过 v8 库运行 js 代码的步骤:

运行 js 代码步骤

-- 步骤 1. 第一步将 js 字符串通过 v8 中的NewFromUtf8Literal方法转换成为Local类型的v8::String, 其中 isolate 是一个 v8 实例,Local 类型为了方便垃圾回收。

  v8::Local<v8::String> source =
     v8::String::NewFromUtf8Literal(isolate, "'Hello' + 'World'");

-- 步骤 2. 第二步将 js 代码进行编译,其中的 Context 是 js 执行的上下文,source 是1中的代码

  v8::Local<v8::Script> script =
          v8::Script::Compile(context, source).ToLocalChecked();

-- 步骤 3. 第三步运行 js 代码。

  v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();

总共分三步:1.字符串类型转换 2.编译 3.运行

向 js 中注入方法

  • emmm。。不过如此,那么如果我们向 js 中注入方法和变量,当然需要对上面的步骤2中 context(JS 执行上下文)做些手脚了,下面我们注入一个 print 方法,首先 print 方法的 C++实现如下,我们不关注具体实现。
   // 这段代码不重要,就知道是C++实现的print方法即可
  void Print(const v8::FunctionCallbackInfo<v8::Value>& args) {
    bool first = true;
    for (int i = 0; i < args.Length(); i++) {
       v8::HandleScope handle_scope(args.GetIsolate());
       if (first) {
         first = false;
       } else {
         printf(" ");
       }
       v8::String::Utf8Value str(args.GetIsolate(), args[i]);
       const char* cstr = ToCString(str);
       printf("%s", cstr);
    }
  printf("\n");
  fflush(stdout);
}
  • Print 方法已经创建完毕,下面需要将该方法加入的 js 的执行上下文中(global)
// 根据v8实例isolate创建一个类型为ObjectTemplate的名字为global的object
v8::Local<v8::ObjectTemplate> global=v8::ObjectTemplate::New(isolate);

// 向上面创建的global中set一个名字为print的方法。简单理解为global.print = Print
global->Set(v8::String::NewFromUtf8(isolate, "print", v8::NewStringType::kNormal).ToLocalChecked(),v8::FunctionTemplate::New(isolate, Print));

// 根据这个global创建对应的context,即js的执行上下文,然后以这个Context再去执行上面的步骤1,步骤2,步骤3.
v8::Local<v8::Context> context = v8::Context::New(isolate, NULL,global);

此时如果再执行

     v8::Local<v8::String> source =
     v8::String::NewFromUtf8Literal(isolate, "print('Hello World')");
     // 三步曲中的Compoile.....
     // 三步曲中的Run....

就能够在 terminal 中看到输出Hello World了。

向 js 中注入变量

和注入方法类似,也是需要向 context(js 执行上下文)中注入变量,但是需要做的是将 C++中的“Object”转换成为 js 中的“Object”。类型转换,前端开发者永远的痛。。

    //和注入方法时一样,先创建Context
   v8::Local<v8::ObjectTemplate> global=v8::ObjectTemplate::New(isolate);
   v8::Local<v8::Context> context = v8::Context::New(isolate, NULL,global);
   // 创建对应的ObjectTemplate,名字为temp1
   Local<v8::ObjectTemplate> templ1 = v8::ObjectTemplate::New(isolate, fun);
   // temp1上加入x属性
   templ1->Set(isolate, "x", v8::Number::New(isolate, 12));
   // temp1上加入y属性
   templ1->Set(isolate, "y",v8::Number::New(isolate, 10));
   // 创建ObjectTemplate的实例instance1
   Local<v8::Object> instance1 =
     templ1->NewInstance(context).ToLocalChecked();
   // 将instance1的内容加入到global.options中
   context->Global()->Set(context, String::NewFromUtf8Literal(isolate, "options"),instance1).FromJust();

此时如果再执行

v8::Local<v8::String> source = v8::String::NewFromUtf8Literal(isolate, "options.x");
// 三步曲中的Compoile.....
// 三步曲中的Run....

就能够在 terminal 中看到输出12了。

从 React-native 源码看 js 和 native 的通讯

现在我们知道了什么是 jsi,也知道了基本的向 js 引擎中注入方法和变量的方法,下一步 We need to dig deeper。

js 到 native 的通讯

  • react-native 的启动流程请看这里有大神详解大神详解,因为我们只关注 JSI 部分,所以直接来到JSIExecutor::initializeRuntime方法。(RN 一顿启动之后会来到这里初始化 runtime),我们将其他几个具体实现省略,只留下第一个nativeModuleProxy的实现。
  void JSIExecutor::initializeRuntime() {
  runtime_->global().setProperty(
      *runtime_,
      "nativeModuleProxy",
      Object::createFromHostObject(
          *runtime_, std::make_shared<NativeModuleProxy>(nativeModules_)));

  runtime_->global().setProperty(
      *runtime_,
      "nativeFlushQueueImmediate",
      Function::createFromHostFunction(
         //具体实现,省略代码
         }));

  runtime_->global().setProperty(
      *runtime_,
      "nativeCallSyncHook",
      Function::createFromHostFunction(
          *runtime_,
          PropNameID::forAscii(*runtime_, "nativeCallSyncHook"),
          1,
           //具体实现,省略代码
           ));

  runtime_->global().setProperty(
      *runtime_,
      "globalEvalWithSourceUrl",
       //具体实现,省略代码
      );
}

代码很容易看懂,就是在 runtime 上面利用 global().setProperty 设置几个模块,以第一个为例,利用 global 的 setProperty 方法在 runtime 的 js context 上加入一个叫做nativeModuleProxy的模块,nativeModuleProxy模块是一个类型为nativeModuleProxy的 Object,里面有一个 get 和 set 方法,就像是我们前端的 proxy 一样,并且所有从 JS to Native 的调用都需要其作为中间代理。

    class JSIExecutor::NativeModuleProxy : public jsi::HostObject {
  public:
  NativeModuleProxy(std::shared_ptr<JSINativeModules> nativeModules)
      : weakNativeModules_(nativeModules) {}

  Value get(Runtime &rt, const PropNameID &name) override {
    if (name.utf8(rt) == "name") {
      return jsi::String::createFromAscii(rt, "NativeModules");
    }

    auto nativeModules = weakNativeModules_.lock();
    if (!nativeModules) {
      return nullptr;
    }
    // 调用getModule
    return nativeModules->getModule(rt, name);
  }

  void set(Runtime &, const PropNameID &, const Value &) override {
    throw std::runtime_error(
        "Unable to put on NativeModules: Operation unsupported");
  }

 private:
  std::weak_ptr<JSINativeModules> weakNativeModules_;
};

在 get 方法中有 getModule 方法,如果你再跳转到 getModule 中能看到其中为 createModule:

 Value JSINativeModules::createModule(Runtime &rt, const PropNameID &name) {
 	//此方法省略了很多。只留一句关键语句,从runtime.global中获得__fbGenNativeModule
 	rt.global().getPropertyAsFunction(rt, "__fbGenNativeModule");
 }

在这个 createModule 中,返回全局定义的__fbGenNativeModule,我们全局搜一下能够搜到在 nativeModules.js 文件中,有定义的__fbGenNativeModule:

global.__fbGenNativeModule = genModule;

接下来再去看 genModule(未贴代码),里面的 genMethod

    function genMethod(moduleID: number, methodID: number, type: MethodType) {
    // 此方法省略至只有return
      return new Promise((resolve, reject) => {
        BatchedBridge.enqueueNativeCall(
          moduleID,
          methodID,
          args,
          data => resolve(data),
          errorData =>
            reject(
              updateErrorWithErrorData(
                (errorData: $FlowFixMe),
                enqueueingFrameError,
              ),
            ),
        );
      });
}

其中的 enqueueNativeCall,再进去看大概就是这样一个方法:

   enqueueNativeCall(xxx) {
       const now = Date.now();
       // MIN_TIME_BETWEEN_FLUSHES_MS = 5
        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);
        }
   }

这里大概做了一个 throttle,如果上次执行 native 和这次执行之间相差大于 5ms,直接执行nativeFlushQueueImmediate。然后再看nativeFlushQueueImmediate

    nativeFlushQueueImmediate() {
          [this](jsi::Runtime &,
          const jsi::Value &,
          const jsi::Value *args,
          size_t count) {
            if (count != 1) {
              throw std::invalid_argument(
                  "nativeFlushQueueImmediate arg count must be 1");
            }
            callNativeModules(args[0], false);
            return Value::undefined();
          }
    }

直接执行的是 callnativeModules 这个方法,这个方法就像是它的名字所述,调用 native 的方法。

综上从 js 到 native 的调用链为:initializeRuntime -> js 侧 setProperty(nativeModuleProxy) -> 在调用 nativeModuleProxy 的时候 -> 触发 nativeModuleProxy 中 get 方法中的 getModule -> createModule -> genModule -> genMethod -> enqueueNativeCall(控制 native 执行频率) -> nativeFlushQueueImmediate -> callNativeModules。

native 到 js 的通讯

我们直接来到 NativeToJsBridge::callFunction 方法,之前的启动顺序可以参考这里,由名字就知道这是一个 native 到 js 的桥,所有从 Native 到 JS 的调用都是从 NativeToJsBridge 中的接口发出去的,看其中调用了 JSCExecutor::callFunction

        // 其中executor是JSExecutor类型的指针,这里指向的是JSIExecutor
       executor->callFunction(module, method, arguments);

再去看 JSIExecutor::callFunction:

void JSIExecutor::callFunction(){
    if (!callFunctionReturnFlushedQueue_) {
      bindBridge();
    }
    scopedTimeoutInvoker_(
      [&] {
          ret = callFunctionReturnFlushedQueue_->call(
              *runtime_,
              moduleId,
              methodId,
              valueFromDynamic(*runtime_, arguments));
        },
        std::move(errorProducer));


     callNativeModules(ret, true);
  }

其中看出如果没有callFunctionReturnFlushedQueue_就会去 bindBridge,如果有的话就回去执行callFunctionReturnFlushedQueue_,那么我们再去看看 bindBridge 中的callFunctionReturnFlushedQueue_到底是什么

    void JSIExecutor::bindBridge() {
    // 省略了大部分代码
    Value batchedBridgeValue =
        runtime_->global().getProperty(*runtime_, "__fbBatchedBridge");
    }

发现和__fbBatchedBridge这个东西有关,全局搜一下,得到:

const BatchedBridge: MessageQueue = new MessageQueue();
Object.defineProperty(global, '__fbBatchedBridge', {
  configurable: true,
  value: BatchedBridge,
});

所以__fbBatchedBridge是一个MessageQueue,打开 messageQueue.js 文件查看 MessageQueue 的callFunctionReturnFlushedQueue方法如下

  callFunctionReturnFlushedQueue(
    module: string,
    method: string,
    args: mixed[],
  ): null | [Array<number>, Array<number>, Array<mixed>, number] {
    this.__guard(() => {
      this.__callFunction(module, method, args);
    });

    return this.flushedQueue();
  }

然后看最终执行是this.__callFunction,再看下这个方法内:

      __callFunction(module: string, method: string, args: mixed[]): void {
            // 省略了大部分代码
            moduleMethods[method].apply(moduleMethods, args);
    }

重要找到了执行 js 方法的地方。。。。
综上从 native 到 js 的调用链为:NativeToJsBridge::callFunction->JSIExecutor::callFunction -> MessageQueue::callFunctionReturnFlushedQueue -> MessageQueue::__callFunction

简述 JSI 的实现

上面我们总结了从 js 到 native 侧相互的调用链,在查看调用链源码的时候,注意到很多方法的参数都有一个名为“runtime”的地址,那么这个 runtime 其实指的就是不同的 JS 引擎,比方说 native 侧需要调用注册在 js 侧的 test 方法,jsi 接口中只是定义了 test 方法,在其内部根据 js 引擎的不同调用不同 runtime 的具体 test 方法的实现,我们拿一个最容易理解的 setProperty 方法为例:首先打开react-native/ReactCommon/jsi/jsi/jsi-inl.h文件看一下 jsi 中定义的setProperty接口方法。

void Object::setProperty(Runtime& runtime, const String& name, T&& value) {
  setPropertyValue(
      runtime, name, detail::toValue(runtime, std::forward<T>(value)));
}

然后再看setPropertyValue,其实现为:

   void setPropertyValue(Runtime& runtime, const String& name, const Value& value) {
    return runtime.setPropertyValue(*this, name, value);
  }

从上面的代码可以看出最终调用的是 runtime(js 引擎)的setPropertyValue方法。
然后我们打开react-native/ReactCommon/jsi/JSCRuntime.cpp文件,该文件为 react-native 默认的 JSC 引擎中 JSI 各方法的具体实现:

    // 具体实现我们不看。只需知道在JSCRuntime中需要实现setPropertyValue方法
    void JSCRuntime::setPropertyValue(
    jsi::Object &object,
    const jsi::PropNameID &name,
    const jsi::Value &value) {
      JSValueRef exc = nullptr;
      JSObjectSetProperty(
      ctx_,
      objectRef(object),
      stringRef(name),
      valueRef(value),
      kJSPropertyAttributeNone,
      &exc);
  checkException(exc);
}

然后我们再打开react-native-v8仓库,该仓库由网上大神实现的 v8 的 react-native runtime 实现,我们打开文件react-native/react-native-v8/src/v8runtime/V8Runtime.cpp看下在 v8 下的具体实现:

    void V8Runtime::setPropertyValue(
    jsi::Object &object,
    const jsi::PropNameID &name,
    const jsi::Value &value) {
    // 具体实现我们不看。只需知道在V8runtime中需要实现setPropertyValue方法
      v8::HandleScope scopedIsolate(isolate_);
      v8::Local<v8::Object> v8Object =
          JSIV8ValueConverter::ToV8Object(*this, object);

      if (v8Object
          ->Set(
              isolate_->GetCurrentContext(),
              JSIV8ValueConverter::ToV8String(*this, name),
              JSIV8ValueConverter::ToV8Value(*this, value))
          .IsNothing()) {
      throw jsi::JSError(*this, "V8Runtime::setPropertyValue failed.");
  }
}

最后我们再打开 hermes 的repo,查看文件/hermes/hermes/API/hermes/hermes.cpp看下在 hermes 下的具体实现:

     void HermesRuntimeImpl::setPropertyValue(
         // 具体实现我们不看。只需知道在hermes中需要实现setPropertyValue方法
        jsi::Object &obj,
        const jsi::String &name,
        const jsi::Value &value) {
      return maybeRethrow([&] {
        vm::GCScope gcScope(&runtime_);
        auto h = handle(obj);
        checkStatus(h->putComputed_RJS(
                         h,
                         &runtime_,
                         stringHandle(name),
                         vmHandleFromValue(value),
                         vm::PropOpFlags().plusThrowOnError())
                        .getStatus());
      });
    }

由此得出在三个引擎上需要分别实现setPropertyValue方法,并在 JSI 接口中声明setProperty方法。

Tags

super_haochen

Peace && love

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.