RN通信流程- JS与原生互调

一、RN整体架构设计

RN架构 .png
JSI交互流程.png

二、JS调用原生及回调

1. 导出原生模块

如何导出?

  • iOS: 类通过RCT_EXTERN_MODULE宏来进行标记 ,方法则通过RCT_EXTERN_METHOD宏标记 ,如果是 UI控件,需要继承RCTUIViewManager并自己实现View
  • Android: 类加注解@ReactModule,继承自ReactContextBaseJavaModule , 方法加注解 @ReactMethod

RN里面如何收集这些导出的方法和模块?

iOS: RCTBridge里面定义了一些方法, RCT_EXTERN_MODULE 和RCT_EXTERN_METHOD实际上调用了这些方法把模块和方法名存到了数组里面,再经过RCTCxxBridge的加工(对各个模块进行注册和实例化),放到了ModuleRegistry里面,这个ModuleRegistry被JSToNativeBridge持有(C++实现),JSToNativeBridge又被RCTCxxBridge持有。

Android: Android这边复杂一点,为了实现跟iOS代码复用,中间有一层JNI转换。 首先Java层有一个CatalystInstance, 他对应的实现CatalystInstanceImpl (相当于iOS的RCTCxxBridge)持有NativeModuleRegistry,NativeModuleRegistery里面通过解析Anotation拿到添加了@ReactModule的 原生模块,以及添加了@ReactMethod 的方法。然后CatalystInstanceImpl通过JNI,把模块信息传递给C++这边的CatalystInstanceImpl,C++这边的CatalystInstanceImpl也有一个ModuleRegistery(跟iOS的一样),有着类似的结构。

关于方法的识别,这里以iOS为例,在模块加载的时候会根据方法名称里面所带的参数类型来生成方法签名,这里面参数类型如果含有 RCTPromiseResolveBlock 和RCTPromiseRejectBlock,则添加一个argumentBlock(invokeWithBridge方法会调用),这个argumentBlock里面再调用 enqueueCallBack添加一个JS回调, 把执行结果或者是错误信息返回给JS。 具体代码在RCTModuleMethod.mm的processMethodSignature函数里, 这个函数做了很多事情,包括方法签名的解析,解析过程比较复杂,这里不贴了。这个解析过程会缓存下来,存到argumentBlocks里面,后续再调用这个方法都读取缓存中的argumentBlocks。

总的来说通过宏或者注解的方式让 RN Instance获取到模块和方法信息,存储到moduleRegistry里, 然后把这些信息转成数组传递给JS, JS这边生成全局的NativeModules来存储这些信息(只存储模块名称、ID和方法名称、ID、参数等相关信息,不存储具体实现)。

JS这边根据方法的类型(原生这边根据参数判断生成的)生成对应的方法原型: 同步/异步/普通方法,其中同步方法是立即调用,其他都要经过messageQueue.js 排队调用

借用一下别人的图

2. JS调用原生流程

NativeModules默认是懒加载的,也就是说第一次require的时候才会进行加载。 JS这边调用Nativemodules["模块名"]["方法名"]时,会在NativeModules里面查找对应方法有无缓存,如果没有,会先去原生这边获取模块相关信息并生成,如果有,则直接调用。 具体可以看一下NativeModules.js

messageQuque.js 负责原生方法的排队调用 ,主要逻辑在enqueueNativeCall这个方法里面, 原生方法是一批一批的调用的, 调用的最小时间间隔是5毫秒。实际调用是通过一个叫nativeFlushQueueImmediate的方法进行操作的,这个方法通过JSCore跟原生进行了绑定。 这个方法的参数被封装到了queue里面,queue是一个数组,原型为

_queue: [number[], number[], any[], number];// 四个元素分别为ModuleIds、 methodIds、params、callId
const MODULE_IDS = 0;
const METHOD_IDS = 1;
const PARAMS = 2;
const MIN_TIME_BETWEEN_FLUSHES_MS = 5;

可以看出,前面三个参数都是数组,也就是说,当多个方法批量调用时,会拆除其moduleId、methodId、params放到对应的数组里面

this._queue[MODULE_IDS].push(moduleID);
this._queue[METHOD_IDS].push(methodID);
this._queue[PARAMS].push(params);

最后一个参数是callId,一般情况下是放在params里面一起传递的,只有一种情况需要特殊处理的,就是等到的时间到了,而原生还没有主动回调时,需要JS主动触发,才传递这个参数,并且这时候其他参数是空的

const now = Date.now();
   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);
   }

那正常放到队列里面的方法什么时候调用呢? 这个就比较复杂了。看JS代码,可以发现 callFunction这个方法触发了原生的调用,callFunction又是通过callFunctionReturnFlushedQueue或者callFunctionReturnResultAndFlushedQueue 来调用的。

__callFunction(module: string, method: string, args: any[]): any {
  this._lastFlush = Date.now();
  this._eventLoopStartTime = this._lastFlush;
  if (__DEV__ || this.__spy) {
    Systrace.beginEvent(`${module}.${method}(${stringifySafe(args)})`);
  } else {
    Systrace.beginEvent(`${module}.${method}(...)`);
  }
  if (this.__spy) {
    this.__spy({type: TO_JS, module, method, args});
  }
  const moduleMethods = this.getCallableModule(module);
  invariant(
    !!moduleMethods,
    'Module %s is not a registered callable module (calling %s)',
    module,
    method,
  );
  invariant(
    !!moduleMethods[method],
    'Method %s does not exist on module %s',
    method,
    module,
  );
  const result = moduleMethods[method].apply(moduleMethods, args); //实际调用原生代码的地方
  Systrace.endEvent();
  return result;
}
 
 
 
callFunctionReturnFlushedQueue(module: string, method: string, args: any[]) {
  this.__guard(() => {
    this.__callFunction(module, method, args);
  });
 
 
  return this.flushedQueue();
}
 
callFunctionReturnResultAndFlushedQueue(
  module: string,
  method: string,
  args: any[],
) {
  let result;
  this.__guard(() => {
    result = this.__callFunction(module, method, args);
  });
 
  return [result, this.flushedQueue()];
}

这两个如果你全局搜索,会发现他们其实也是通过原生来调用的,在原生这边可以看到JSIExecutor::callFunction这么一个方法。这个方法是用于原生主动调用JS代码的,这里面有一个 callFunctionReturnFlushedQueue_->call的调用。在这外面还有一个

scopedTimeoutInvoker_,用于延迟调用,具体为什么要延迟我们先不管。总的来说,JS这边触发原生调用是需要定时器或者是>=5ms才触发

void JSIExecutor::callFunction(
    const std::string& moduleId,
    const std::string& methodId,
    const folly::dynamic& arguments) {
  SystraceSection s(
      "JSIExecutor::callFunction", "moduleId", moduleId, "methodId", methodId);
  if (!callFunctionReturnFlushedQueue_) {
    bindBridge();
  }
 
 
  // Construct the error message producer in case this times out.
  // This is executed on a background thread, so it must capture its parameters
  // by value.
  auto errorProducer = [=] {
    std::stringstream ss;
    ss << "moduleID: " << moduleId << " methodID: " << methodId
       << " arguments: " << folly::toJson(arguments);
    return ss.str();
  };
 
 
  Value ret = Value::undefined();
  try {
    scopedTimeoutInvoker_(
        [&] {
          ret = callFunctionReturnFlushedQueue_->call(
              *runtime_,
              moduleId,
              methodId,
              valueFromDynamic(*runtime_, arguments));
        },
        std::move(errorProducer));
  } catch (...) {
    std::throw_with_nested(
        std::runtime_error("Error calling " + moduleId + "." + methodId));
  }
 
 
  callNativeModules(ret, true);
}
image.png

经过JSCore的转换, 方法最终调用到原生这边来。 到原生之后,会通过JSI层的 callNativeModules方法,调用到JSToNativeBridge这边来。然后调用moduleRegistry的callNativeModules

void callNativeModules(
      JSExecutor& executor, folly::dynamic&& calls, bool isEndOfBatch) override {
 
 
    CHECK(m_registry || calls.empty()) <<
      "native module calls cannot be completed with no native modules";
    m_batchHadNativeModuleCalls = m_batchHadNativeModuleCalls || !calls.empty();
 
 
    // An exception anywhere in here stops processing of the batch.  This
    // was the behavior of the Android bridge, and since exception handling
    // terminates the whole bridge, there's not much point in continuing.
    for (auto& call : parseMethodCalls(std::move(calls))) {
      m_registry->callNativeMethod(call.moduleId, call.methodId, std::move(call.arguments), call.callId); //调用对应方法
    }
    if (isEndOfBatch) {
      // onBatchComplete will be called on the native (module) queue, but
      // decrementPendingJSCalls will be called sync. Be aware that the bridge may still
      // be processing native calls when the birdge idle signaler fires.
      if (m_batchHadNativeModuleCalls) {
        m_callback->onBatchComplete(); //批量调用结束
        m_batchHadNativeModuleCalls = false;
      }
      m_callback->decrementPendingJSCalls();
    }
  }
void ModuleRegistry::callNativeMethod(unsigned int moduleId, unsigned int methodId, folly::dynamic&& params, int callId) {
  if (moduleId >= modules_.size()) {
    throw std::runtime_error(
      folly::to<std::string>("moduleId ", moduleId, " out of range [0..", modules_.size(), ")"));
  }
  modules_[moduleId]->invoke(methodId, std::move(params), callId);
}

到这里IOS和android就有一个分化了。如果是iOS,会通过 RCTNativeModule的invoke方法,间接调用到RCTModuleMethod的invokeWithBridge , 生成对应的NSInvocation执行对应原生方法的调用。

如果是Android,则通过Java层传递到instance的NativeModule来调用,这个invoke实际调用的是NativeModule.java里面的invoke,这是NativeMethod接口提供的方法,实际上的实现在JavaMethodWrapper.java里面,通过运行时反射机制触发的对应的原生方法。

在调用完各个模块的方法之后,还会有一个 m_callback 的onBatchComplete方法,回调到OC和Java层的onBatchComplete。这个主要是供UIManager刷新用的,这里就不展开讲了。

总结

  • JSIExecutor、MessageQueue是两端交互的核心,通过这两者注入代理对象供另一方调用,以实现Native&JS数据传递、互相调用。

  • JS call Native的触发时机有:

    • 1.调用enqueueNativeCall函数入队(存入暂存表)时发现距离上一次调用大于5毫秒时,通过nativeFlushQueueImmediate执行调用;
    • 2.执行flushedQueue时(flushedQueue用于执行JS端setImmediate异步任务,在此不展开讨论),把原生模块调用信息作为返回值传递到原生端,执行调用;
    • 3.通过callFunctionReturnFlushedQueue执行JS call Native也会触发flushedQueue,同样返回原生模块调用信息
    • 4.通过invokeCallbackAndReturnFlushedQueue执行JS回调,同理。
      笔者猜想这种设计的目的是:保证能及时发起函数调用的前提下,减少调用频率。毕竟 JS call Native的调用是非常频繁的。

三、原生调用JS

原生调用JS.jpeg

1.一般调用

相比JS调原生,原生调JS则简单的多,NativeToJSBridge有一个callFunction方法,其内部是调用了JSIExecutor的 callFunction,而callFunction前面已经讲过了他的逻辑。NativeToJSBridge又被RCTCxxBridge和RCTBridge封装了两层,最终暴露给开发者的是enqueueJSCall这个方法。这里为什么要enqueue呢? 因为原生和JS代码是运行在不同的线程,原生要调用JS,需要切换线程,也就是切到了对应的MessageQueue上。iOS这边是RCTMessageThread(被RCTCxxBridge持有的_jsThread),android这边是MessageQueueThread(被CatalystInstanceImpl持有的mNativeModulesQueueThread)。然后通过消息队列,触发对应的函数执行。

这里有一个需要提及的,就是JS调用原生之后,promise的回调,这个是如何实现的?

前面我们已经看到JS调用C++的时候会在参数里面传callbackId过来,这个callBackID实际由两部分组成,一个succCallId,一个failCallId,通过对callId移位操作得到(一个左移,一个右移)

原生这边,前面已经讲过,在模块加载的时候会根据方法名称里面所带的参数类型来识别是否需要回调,并生成对应的argumentBlock,等待原生桥接方法执行完成然后再调用,这里截取RCTModuleMethod的部分代码看下逻辑

else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) {
      RCTAssert(i == numberOfArguments - 2,
                @"The RCTPromiseResolveBlock must be the second to last parameter in %@",
                [self methodName]);
      BLOCK_CASE((id result), {
        [bridge enqueueCallback:json args:result ? @[result] : @[]];
      });
    } else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) {
      RCTAssert(i == numberOfArguments - 1,
                @"The RCTPromiseRejectBlock must be the last parameter in %@",
                [self methodName]);
      BLOCK_CASE((NSString *code, NSString *message, NSError *error), {
        NSDictionary *errorJSON = RCTJSErrorFromCodeMessageAndNSError(code, message, error);
        [bridge enqueueCallback:json args:@[errorJSON]];
      });
    }

到JS这边,生成方法的时候是会生成并返回promise的,并且对应的resolve和reject也已经生成,原生这边只需要根据参数对应的位置直接调用即可。

function genMethod(moduleID: number, methodID: number, type: MethodType) {
  let fn = null;
  if (type === 'promise') {
    fn = function(...args: Array<any>) {
      return new Promise((resolve, reject) => {
        BatchedBridge.enqueueNativeCall(
          moduleID,
          methodID,
          args,
          data => resolve(data),
          errorData => reject(createErrorFromErrorData(errorData)),
        );
      });
    };
  }
.......
}

2. 通知调用

原生调用JS还有另外一种方式, 即通过RCTDeviceEventEmitter 发通知。这种方式其实本质上跟直接调用enqueueJSCall没太大区别,只不过封装了一个观察者,使其可以达到一个程度上的解耦。我们看下他的实现

- (void)sendEventWithName:(NSString *)eventName body:(id)body
{
  RCTAssert(_bridge != nil, @"Error when sending event: %@ with body: %@. "
            "Bridge is not set. This is probably because you've "
            "explicitly synthesized the bridge in %@, even though it's inherited "
            "from RCTEventEmitter.", eventName, body, [self class]);
 
 
  if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) {
    RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`",
                eventName, [self class], [[self supportedEvents] componentsJoinedByString:@"`, `"]);
  }
  if (_listenerCount > 0) {
    [_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
                    method:@"emit"
                      args:body ? @[eventName, body] : @[eventName]
                completion:NULL];
  } else {
    RCTLogWarn(@"Sending `%@` with no listeners registered.", eventName);
  }
}

在JS这边RCTDeviceEventEmitter是继承自EventEmitter的

class RCTDeviceEventEmitter extends EventEmitter

EventEmitter也很简单,这里只贴一个函数,就是emit,可以看出,这个函数就是把传入的eventName、body拿出来,然后通过listener的apply直接调用对应的实现,这个listener是EmitterSubscription的属性, 会根据eventType过滤对应的消息

/**
   * Emits an event of the given type with the given data. All handlers of that
   * particular type will be notified.
   *
   * @param {string} eventType - Name of the event to emit
   * @param {...*} Arbitrary arguments to be passed to each registered listener
   *
   * @example
   *   emitter.addListener('someEvent', function(message) {
   *     console.log(message);
   *   });
   *
   *   emitter.emit('someEvent', 'abc'); // logs 'abc'
   */
  emit(eventType: string) {
    const subscriptions: ?[EmitterSubscription] = (this._subscriber.getSubscriptionsForType(eventType): any);
    if (subscriptions) {
      for (let i = 0, l = subscriptions.length; i < l; i++) {
        const subscription = subscriptions[i];
 
 
        // The subscription may have been removed during this event loop.
        if (subscription) {
          this._currentSubscription = subscription;
          subscription.listener.apply(
            subscription.context,
            Array.prototype.slice.call(arguments, 1)
          );
        }
      }
      this._currentSubscription = null;
    }
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容