RN通信流程- JS與原生互調(diào)

一、RN整體架構(gòu)設(shè)計

RN架構(gòu) .png
JSI交互流程.png

二、JS調(diào)用原生及回調(diào)

1. 導(dǎo)出原生模塊

如何導(dǎo)出?

  • iOS: 類通過RCT_EXTERN_MODULE宏來進行標(biāo)記 ,方法則通過RCT_EXTERN_METHOD宏標(biāo)記 ,如果是 UI控件,需要繼承RCTUIViewManager并自己實現(xiàn)View
  • Android: 類加注解@ReactModule,繼承自ReactContextBaseJavaModule , 方法加注解 @ReactMethod

RN里面如何收集這些導(dǎo)出的方法和模塊?

iOS: RCTBridge里面定義了一些方法, RCT_EXTERN_MODULE 和RCT_EXTERN_METHOD實際上調(diào)用了這些方法把模塊和方法名存到了數(shù)組里面,再經(jīng)過RCTCxxBridge的加工(對各個模塊進行注冊和實例化),放到了ModuleRegistry里面,這個ModuleRegistry被JSToNativeBridge持有(C++實現(xiàn)),JSToNativeBridge又被RCTCxxBridge持有。

Android: Android這邊復(fù)雜一點,為了實現(xiàn)跟iOS代碼復(fù)用,中間有一層JNI轉(zhuǎn)換。 首先Java層有一個CatalystInstance, 他對應(yīng)的實現(xiàn)CatalystInstanceImpl (相當(dāng)于iOS的RCTCxxBridge)持有NativeModuleRegistry,NativeModuleRegistery里面通過解析Anotation拿到添加了@ReactModule的 原生模塊,以及添加了@ReactMethod 的方法。然后CatalystInstanceImpl通過JNI,把模塊信息傳遞給C++這邊的CatalystInstanceImpl,C++這邊的CatalystInstanceImpl也有一個ModuleRegistery(跟iOS的一樣),有著類似的結(jié)構(gòu)。

關(guān)于方法的識別,這里以iOS為例,在模塊加載的時候會根據(jù)方法名稱里面所帶的參數(shù)類型來生成方法簽名,這里面參數(shù)類型如果含有 RCTPromiseResolveBlock 和RCTPromiseRejectBlock,則添加一個argumentBlock(invokeWithBridge方法會調(diào)用),這個argumentBlock里面再調(diào)用 enqueueCallBack添加一個JS回調(diào), 把執(zhí)行結(jié)果或者是錯誤信息返回給JS。 具體代碼在RCTModuleMethod.mm的processMethodSignature函數(shù)里, 這個函數(shù)做了很多事情,包括方法簽名的解析,解析過程比較復(fù)雜,這里不貼了。這個解析過程會緩存下來,存到argumentBlocks里面,后續(xù)再調(diào)用這個方法都讀取緩存中的argumentBlocks。

總的來說通過宏或者注解的方式讓 RN Instance獲取到模塊和方法信息,存儲到moduleRegistry里, 然后把這些信息轉(zhuǎn)成數(shù)組傳遞給JS, JS這邊生成全局的NativeModules來存儲這些信息(只存儲模塊名稱、ID和方法名稱、ID、參數(shù)等相關(guān)信息,不存儲具體實現(xiàn))。

JS這邊根據(jù)方法的類型(原生這邊根據(jù)參數(shù)判斷生成的)生成對應(yīng)的方法原型: 同步/異步/普通方法,其中同步方法是立即調(diào)用,其他都要經(jīng)過messageQueue.js 排隊調(diào)用

借用一下別人的圖

2. JS調(diào)用原生流程

NativeModules默認(rèn)是懶加載的,也就是說第一次require的時候才會進行加載。 JS這邊調(diào)用Nativemodules["模塊名"]["方法名"]時,會在NativeModules里面查找對應(yīng)方法有無緩存,如果沒有,會先去原生這邊獲取模塊相關(guān)信息并生成,如果有,則直接調(diào)用。 具體可以看一下NativeModules.js

messageQuque.js 負責(zé)原生方法的排隊調(diào)用 ,主要邏輯在enqueueNativeCall這個方法里面, 原生方法是一批一批的調(diào)用的, 調(diào)用的最小時間間隔是5毫秒。實際調(diào)用是通過一個叫nativeFlushQueueImmediate的方法進行操作的,這個方法通過JSCore跟原生進行了綁定。 這個方法的參數(shù)被封裝到了queue里面,queue是一個數(shù)組,原型為

_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;

可以看出,前面三個參數(shù)都是數(shù)組,也就是說,當(dāng)多個方法批量調(diào)用時,會拆除其moduleId、methodId、params放到對應(yīng)的數(shù)組里面

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

最后一個參數(shù)是callId,一般情況下是放在params里面一起傳遞的,只有一種情況需要特殊處理的,就是等到的時間到了,而原生還沒有主動回調(diào)時,需要JS主動觸發(fā),才傳遞這個參數(shù),并且這時候其他參數(shù)是空的

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);
   }

那正常放到隊列里面的方法什么時候調(diào)用呢? 這個就比較復(fù)雜了??碕S代碼,可以發(fā)現(xiàn) callFunction這個方法觸發(fā)了原生的調(diào)用,callFunction又是通過callFunctionReturnFlushedQueue或者callFunctionReturnResultAndFlushedQueue 來調(diào)用的。

__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); //實際調(diào)用原生代碼的地方
  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()];
}

這兩個如果你全局搜索,會發(fā)現(xiàn)他們其實也是通過原生來調(diào)用的,在原生這邊可以看到JSIExecutor::callFunction這么一個方法。這個方法是用于原生主動調(diào)用JS代碼的,這里面有一個 callFunctionReturnFlushedQueue_->call的調(diào)用。在這外面還有一個

scopedTimeoutInvoker_,用于延遲調(diào)用,具體為什么要延遲我們先不管??偟膩碚f,JS這邊觸發(fā)原生調(diào)用是需要定時器或者是>=5ms才觸發(fā)。

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

經(jīng)過JSCore的轉(zhuǎn)換, 方法最終調(diào)用到原生這邊來。 到原生之后,會通過JSI層的 callNativeModules方法,調(diào)用到JSToNativeBridge這邊來。然后調(diào)用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); //調(diào)用對應(yīng)方法
    }
    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(); //批量調(diào)用結(jié)束
        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方法,間接調(diào)用到RCTModuleMethod的invokeWithBridge , 生成對應(yīng)的NSInvocation執(zhí)行對應(yīng)原生方法的調(diào)用。

如果是Android,則通過Java層傳遞到instance的NativeModule來調(diào)用,這個invoke實際調(diào)用的是NativeModule.java里面的invoke,這是NativeMethod接口提供的方法,實際上的實現(xiàn)在JavaMethodWrapper.java里面,通過運行時反射機制觸發(fā)的對應(yīng)的原生方法。

在調(diào)用完各個模塊的方法之后,還會有一個 m_callback 的onBatchComplete方法,回調(diào)到OC和Java層的onBatchComplete。這個主要是供UIManager刷新用的,這里就不展開講了。

總結(jié)

  • JSIExecutor、MessageQueue是兩端交互的核心,通過這兩者注入代理對象供另一方調(diào)用,以實現(xiàn)Native&JS數(shù)據(jù)傳遞、互相調(diào)用。

  • JS call Native的觸發(fā)時機有:

    • 1.調(diào)用enqueueNativeCall函數(shù)入隊(存入暫存表)時發(fā)現(xiàn)距離上一次調(diào)用大于5毫秒時,通過nativeFlushQueueImmediate執(zhí)行調(diào)用;
    • 2.執(zhí)行flushedQueue時(flushedQueue用于執(zhí)行JS端setImmediate異步任務(wù),在此不展開討論),把原生模塊調(diào)用信息作為返回值傳遞到原生端,執(zhí)行調(diào)用;
    • 3.通過callFunctionReturnFlushedQueue執(zhí)行JS call Native也會觸發(fā)flushedQueue,同樣返回原生模塊調(diào)用信息
    • 4.通過invokeCallbackAndReturnFlushedQueue執(zhí)行JS回調(diào),同理。
      筆者猜想這種設(shè)計的目的是:保證能及時發(fā)起函數(shù)調(diào)用的前提下,減少調(diào)用頻率。畢竟 JS call Native的調(diào)用是非常頻繁的。

三、原生調(diào)用JS

原生調(diào)用JS.jpeg

1.一般調(diào)用

相比JS調(diào)原生,原生調(diào)JS則簡單的多,NativeToJSBridge有一個callFunction方法,其內(nèi)部是調(diào)用了JSIExecutor的 callFunction,而callFunction前面已經(jīng)講過了他的邏輯。NativeToJSBridge又被RCTCxxBridge和RCTBridge封裝了兩層,最終暴露給開發(fā)者的是enqueueJSCall這個方法。這里為什么要enqueue呢? 因為原生和JS代碼是運行在不同的線程,原生要調(diào)用JS,需要切換線程,也就是切到了對應(yīng)的MessageQueue上。iOS這邊是RCTMessageThread(被RCTCxxBridge持有的_jsThread),android這邊是MessageQueueThread(被CatalystInstanceImpl持有的mNativeModulesQueueThread)。然后通過消息隊列,觸發(fā)對應(yīng)的函數(shù)執(zhí)行。

這里有一個需要提及的,就是JS調(diào)用原生之后,promise的回調(diào),這個是如何實現(xiàn)的?

前面我們已經(jīng)看到JS調(diào)用C++的時候會在參數(shù)里面?zhèn)鱟allbackId過來,這個callBackID實際由兩部分組成,一個succCallId,一個failCallId,通過對callId移位操作得到(一個左移,一個右移)

原生這邊,前面已經(jīng)講過,在模塊加載的時候會根據(jù)方法名稱里面所帶的參數(shù)類型來識別是否需要回調(diào),并生成對應(yīng)的argumentBlock,等待原生橋接方法執(zhí)行完成然后再調(diào)用,這里截取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的,并且對應(yīng)的resolve和reject也已經(jīng)生成,原生這邊只需要根據(jù)參數(shù)對應(yīng)的位置直接調(diào)用即可。

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. 通知調(diào)用

原生調(diào)用JS還有另外一種方式, 即通過RCTDeviceEventEmitter 發(fā)通知。這種方式其實本質(zhì)上跟直接調(diào)用enqueueJSCall沒太大區(qū)別,只不過封裝了一個觀察者,使其可以達到一個程度上的解耦。我們看下他的實現(xiàn)

- (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也很簡單,這里只貼一個函數(shù),就是emit,可以看出,這個函數(shù)就是把傳入的eventName、body拿出來,然后通過listener的apply直接調(diào)用對應(yīng)的實現(xiàn),這個listener是EmitterSubscription的屬性, 會根據(jù)eventType過濾對應(yīng)的消息

/**
   * 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;
    }
  }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容