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

Javascript 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方法。

本文作者与 https://quickapp.vivo.com.cn/react/ 为同一作者
reference:http://zxfcumtcs.github.io/2017/10/08/ReactNativeCommunicationMechanism/
https://zxfcumtcs.github.io/2017/10/12/ReactNativeCommunicationMechanism2/#more

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.