本文提纲:

  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

此外,moduleMethods是之前启动时有一个模块懒注册的过程,所以moduleMethods中保存了相应的方法

//react-native/Libraries/BatchedBridge/MessageQueue.js文件中
  registerLazyCallableModule(name: string, factory: void => {...}) {
    let module: {...};
    let getValue: ?(void) => {...} = factory;
    this._lazyCallableModules[name] = () => {
      if (getValue) {
        module = getValue();
        getValue = null;
      }
      return module;
    };
  }
  
  
// react-native/Libraries/Core/setUpBatchedBridge.js文件中
    registerModule = (moduleName, factory) =>
        BatchedBridge.registerLazyCallableModule(moduleName, factory);
    }

    registerModule('Systrace', () => require('../Performance/Systrace'));
    registerModule('JSTimers', () => require('./Timers/JSTimers'));
    registerModule('HeapCapture', () => require('../HeapCapture/HeapCapture'));
    registerModule('SamplingProfiler', () =>
      require('../Performance/SamplingProfiler'),
    );
    registerModule('RCTLog', () => require('../Utilities/RCTLog'));

简述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