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閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容