ReactNative源碼解析——通訊機制詳解(callback、線程)

本文經過分析 RN 源碼,簡要介紹了 JS to Native 的 callback 實現原理以及 RN 中的三個重要線程。react

本文同時發表於個人我的博客git

callback


前兩篇文章ReactNative源碼解析——通訊機制詳解(1/2) ReactNative源碼解析——通訊機制詳解(2/2)分別介紹了 RN 通訊機制中的 JS to Native、Native to JS 的執行流程。爲了集中注意力抓住主要流程,當時沒有分析調用過程當中的 callback 問題,下面簡要分析一下 JS to Native callback 的實現原理。github

文中所列代碼均作了簡化處理。json

首先,來看一個具體的例子:數組

RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options
                  failureCallback:(RCTResponseErrorBlock)failureCallback
                  successCallback:(RCTResponseSenderBlock)successCallback)
{
    UIActivityViewController *shareController =
    [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil];

    shareController.completionWithItemsHandler = 
    ^(NSString *activityType, 
       BOOL completed, 
       __unused NSArray *returnedItems, 
       NSError *activityError) {
           if (activityError) {
               failureCallback(activityError);
           } 
           else {
               successCallback(@[@(completed), RCTNullIfNil(activityType)]);
           }
       };
}
複製代碼

showShareActionSheetWithOptions:failureCallback:successCallback:RCTActionSheetManager曝露給 JS 的方法之一,其包含兩個 callback:failureCallbacksuccessCallback。 其中,RCTResponseErrorBlockRCTResponseSenderBlock的定義以下:app

/** * The type of a block that is capable of sending a response to a bridged * operation. Use this for returning callback methods to JS. */
typedef void (^RCTResponseSenderBlock)(NSArray *response);

/** * The type of a block that is capable of sending an error response to a * bridged operation. Use this for returning error information to JS. */
typedef void (^RCTResponseErrorBlock)(NSError *error);
複製代碼

JS 中的調用:ide

ActionSheetIOS.showShareActionSheetWithOptions({
        url: uri,
        excludedActivityTypes: [
          'com.apple.UIKit.activity.PostToTwitter'
        ]
      },
      (error) => alert(error),
      (completed, method) => {
        var text;
        if (completed) {
          text = `Shared via ${method}`;
        } else {
          text = 'You didn\'t share';
        }
        this.setState({text});
      });
複製代碼

上述代碼的第7行、8~16行,分別設置了兩個 fail、success callback。 oop

上圖所示是 JS to Native 中最終在 Native 側調用相應方法的調用棧,在 ReactNative源碼解析——通訊機制詳解(1/2)中提到過,但限於篇幅沒有展開討論。

今天的分析就從RCTModuleMethod#invokeWithBridge:module:arguments:開始。ui

RCTModuleMethod#invokeWithBridge:

- (id)invokeWithBridge:(RCTBridge *)bridge
                module:(id)module
             arguments:(NSArray *)arguments
{
    if (_argumentBlocks == nil) {
        [self processMethodSignature];
    }

    // Set arguments
    //
    NSUInteger index = 0;
    for (id json in arguments) {
        RCTArgumentBlock block = _argumentBlocks[index];
        if (!block(bridge, index, RCTNilIfNull(json))) {
            return nil;
        }
        index++;
    }

    // Invoke method
    //
    [_invocation invokeWithTarget:module];
    return nil;
}
複製代碼

invokeWithBridge:module:arguments:內部(第6行)調用了processMethodSignature方法,從名稱可知該方法是處理『方法簽名』的(被處理的方法固然是被 JS 調用的 Native method 了):this

- (void)processMethodSignature
{
    NSArray<RCTMethodArgument *> *arguments;
    _selector = RCTParseMethodSignature(_methodInfo->objcName, &arguments);

    // Create method invocation
    NSMethodSignature *methodSignature =  [_moduleClass instanceMethodSignatureForSelector:_selector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    invocation.selector = _selector;
    _invocation = invocation;

    // Process arguments
    //
    NSUInteger numberOfArguments = methodSignature.numberOfArguments;
    NSMutableArray<RCTArgumentBlock> *argumentBlocks =
    [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2];

    for (NSUInteger i = 2; i < numberOfArguments; i++) {
        const char *objcType = [methodSignature getArgumentTypeAtIndex:i];
        RCTMethodArgument *argument = arguments[i - 2];
        NSString *typeName = argument.type;
        SEL selector = RCTConvertSelectorForType(typeName);
        if ([RCTConvert respondsToSelector:selector]) {
            switch (objcType[0]) {
                case _C_CHR: {
                    char (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend;
                    [argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) {
                        char value = convert([RCTConvert class], selector, json);
                        [invocation setArgument:&value atIndex:(index) + 2];
                        return YES;}];
                        break;
                }
            }
        }
        else if ([typeName isEqualToString:@"RCTResponseSenderBlock"]) {
            [argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) {
                void (^block)(NSArray *) = ^(NSArray *args) {
                    [bridge enqueueCallback:json args:args];
                };
                id value = json ? [block copy] : (id)^(__unused NSArray *_){};
                CFBridgingRetain(value);

                [invocation setArgument:&value atIndex:(index) + 2];
                return YES;
            }];
        } 
        // ...
    }
    _argumentBlocks = argumentBlocks;
}
複製代碼

processMethodSignature方法主要作的工做:

  • 從被調方法的名稱(字符串)中解析出 selector 以及RCTMethodArgument格式的參數(第4行);
  • 根據第一步中解析出的 selector 生成methodSignature、invocation(第7~10行);
  • 爲每一個參數生成一個 block(argumentBlock),並添加到argumentBlocks數組中: 1. 若參數的類型在 JS 與 Native 間能夠轉換(如:基礎類型、字符串、數組等)(第23~34行),在argumentBlock中完成 JS 類型參數 to Native 類型參數的轉換(第28行),並將獲得的結果設置到invocation上(第29行); 2. 若參數類型是 block(如:RCTResponseSenderBlockRCTResponseErrorBlock等)(第35~46行),則在argumentBlock中生成一個 bolck,用做調用方法時的實參(由於 block 類型沒法從 JS 直接傳給 Native),在該 block 中調用了bridgeenqueueCallback:args:方法。

再回到RCTModuleMethod#invokeWithBridge:module:arguments:方法,第12~18行,執行了processMethodSignature方法爲每一個參數生成的 block,最終的效果就是將 JS 側傳入的參數值轉換成 Native 類型並設置到invocation上。

經過上述分析可知,對於 block 類型的參數(callback)最終會調用了RCTCxxBridge#enqueueCallback:args:方法。

void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) {
    auto result = [&] {
        try {
            return m_invokeCallbackAndReturnFlushedQueueJS->callAsFunction({
                Value::makeNumber(m_context, callbackId),
                Value::fromDynamic(m_context, std::move(arguments))
            });
        } catch (...) {}
    }();
    callNativeModules(std::move(result));
}
複製代碼

沿調用鏈最終來到JSCExecutor::invokeCallback,第4行經過 hook,實際調用的是 JS 側的MessageQueue#invokeCallbackAndReturnFlushedQueue方法。 下面咱們來看看 JS 側如何處理 callback。

NativeModules#genMethod

還記得genMethod方法嗎?

function genMethod(moduleID: number, methodID: number, type: MethodType) {
    let fn = null;
    fn = function(...args: Array<any>) {
        const lastArg = args.length > 0 ? args[args.length - 1] : null;
        const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
        const hasSuccessCallback = typeof lastArg === 'function';
        const hasErrorCallback = typeof secondLastArg === 'function';
        const onSuccess = hasSuccessCallback ? lastArg : null;
        const onFail = hasErrorCallback ? secondLastArg : null;
        const callbackCount = hasSuccessCallback + hasErrorCallback;
        args = args.slice(0, args.length - callbackCount);
        BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess);
    };
    fn.type = type;
    return fn;
}
複製代碼

ReactNative源碼解析——通訊機制詳解(1/2) 一文中介紹過,JS 在調用 Native 方法時,會在 JS 側動態生成一個對應的 JS 方法。 在genMethod方法的第4~9行,處理了調用參數的最後2個,判斷是不是 callback 類型(function 類型)。 從上述處理代碼中能夠得出:

  • 對於 callback 類型的參數,做了特殊處理,將其從參數列表中剝離出來;
  • 一個方法最多隻能有兩個 callback 類型的參數;
  • callback 類型的參數只能位於參數列表的最後。

下面再來看看MessageQueue.enqueueNativeCall

enqueueNativeCall(
    moduleID: number,
    methodID: number,
    params: any[],
    onFail: ?Function,
    onSucc: ?Function,
  ) {
    if (onFail || onSucc) {
        // Encode callIDs into pairs of callback identifiers by shifting left and using the rightmost bit
        // to indicate fail (0) or success (1)
        // eslint-disable-next-line no-bitwise
        onFail && params.push(this._callID << 1);
        // eslint-disable-next-line no-bitwise
        onSucc && params.push((this._callID << 1) | 1);
        this._successCallbacks[this._callID] = onSucc;
        this._failureCallbacks[this._callID] = onFail;
    }
    this._callID++;
}
複製代碼

能夠看到:

  • 對於 callback 類型的參數,在 JS 與 Native 間傳遞的是 callbackID(_successCallbacks_failureCallbacks中的下標);
  • JS 側的 callback function 存儲在_successCallbacks_failureCallbacks中。

咱們再回到調用流程中的MessageQueue#invokeCallbackAndReturnFlushedQueue方法,在該方法中調用了__invokeCallback方法:

__invokeCallback(cbID: number, args: any[]) {
    // The rightmost bit of cbID indicates fail (0) or success (1), the other bits are the callID shifted left.
    // eslint-disable-next-line no-bitwise
    const callID = cbID >>> 1;
    // eslint-disable-next-line no-bitwise
    const isSuccess = cbID & 1;
    const callback = isSuccess
      ? this._successCallbacks[callID]
      : this._failureCallbacks[callID];

    if (!callback) {
      return;
    }

    this._successCallbacks[callID] = this._failureCallbacks[callID] = null;
    callback(...args);
}
複製代碼

__invokeCallback方法中,經過 cbID 在_successCallbacks_failureCallbacks中找到相應的 callback function,並執行。至此,callback 的流程所有結束。

RN 經過 callbackID 的最後一位是0仍是1,肯定callback 是 success 仍是 fail。

小結

  • JS 與 Native 間傳遞的是 callbackID;
  • callback 參數只能位於方法參數列表的最後面而且最多隻能有2個;
  • RN 經過 callbackID 二進制的最後一位是0仍是1,肯定是 success 仍是 fail;
  • 因爲 JS callback function 沒法直接傳遞給 Native,Native 側會生成一個 block。

線程模型


在 RN 中,有3類線程須要關注:

  • JS Thread;
  • Native Module Thread;
  • UI Manager Thread(Shadow Thread)。

JS Thread

JS Thread 是 JS 執行以及 JS 與 Native 通訊線程。 簡單講,Native 在此線程執行 JS 代碼,JS 調用 Native 接口也發生在此線程上。 JS Thread 的初始化發生在RCTCxxBridge#start方法中:

_jsThread = [[NSThread alloc] initWithTarget:[self class]
                                                   selector:@selector(runRunLoop)
                                                     object:nil];
_jsThread.name = RCTJSThreadName;
_jsThread.qualityOfService = NSOperationQualityOfServiceUserInteractive;
[_jsThread start];
複製代碼

在閱讀 RN 源碼時可能會發現RCTMessageThread類,它是對 JS Thread 的 C++封裝。具體源碼就不列了。 還會發現RCTJSThread變量:

dispatch_queue_t RCTJSThread;
RCTJSThread = (id)kCFNull;
複製代碼

NOTE: RCTJSThread is not a real libdispatch queue

RCTJSThread的做用只是用於標識,確保須要在 JSThread 上執行的操做能在該線程上執行:

Native module Thread

JS 在調用 Native 方法時,Native 方法在哪一個線程上執行? Native Module 能夠實現methodQueue方法,指定執行隊列:- (dispatch_queue_t)methodQueue。 那若是 Native Module 沒有實現methodQueue方法,會如何?

- (void)setUpMethodQueue
{
    BOOL implementsMethodQueue = [_instance respondsToSelector:@selector(methodQueue)];
    if (implementsMethodQueue && _bridge.valid) {
        _methodQueue = _instance.methodQueue;
    }
    if (!_methodQueue && _bridge.valid) {
        // Create new queue (store queueName, as it isn't retained by dispatch_queue)
        _queueName = [NSString stringWithFormat:@"com.facebook.react.%@Queue", self.name];
        _methodQueue = dispatch_queue_create(_queueName.UTF8String, DISPATCH_QUEUE_SERIAL);
    }
}
複製代碼

RCTModuleData#setUpMethodQueue方法中能夠看到:若Native Module 沒有實現methodQueue方法,則爲該 Native Module 生成一個串行隊列。 那麼在實現 Native module#methodQueue 方法時須要注意什麼? 來了解一下 RN 自帶的 module 實現狀況:

  • main thread — 如 RCTActionSheetManager,在接口中有 UI 操做;
  • JSThread — 如 RCTTiming、RCTEventDispatcher,實時性要求較高的 (慎用,This can have serious implications for performance, so only use this if you're sure it's what you need);
  • UI Manager thread — 如 RCTUIManager、RCTViewManager,UI 組件;
  • Custom thread — 如 RCTAsyncLocalStorage,耗時操做。

UI Manager Thread(Shadow Thread)

UI Manager Thread,UI 組件(UI module)接口執行線程。 UI 不該該在 main thread? RN 爲了提升效率(如: 幀率),會先在UI Manager Thread作一些預處理操做(如計算 frame),最終在渲染上屏時會切到 main thread。

dispatch_queue_t RCTGetUIManagerQueue(void)
{
    static dispatch_queue_t shadowQueue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if ([NSOperation instancesRespondToSelector:@selector(qualityOfService)]) {
            dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0);
            shadowQueue = dispatch_queue_create(RCTUIManagerQueueName, attr);
        }
        else {
            shadowQueue = dispatch_queue_create(RCTUIManagerQueueName, DISPATCH_QUEUE_SERIAL);
            dispatch_set_target_queue(shadowQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
        }
    });
    return shadowQueue;
}
複製代碼

能夠看到,UI Manager Thread 是一個高優先級的串行隊列。

小結

  • 全部 JS 代碼都會在獨立線程 JSThread 上執行;
  • 可經過 methodQueue 方法自定義 Native module 執行線程;
  • 爲了提升效率,全部 UI 組件都會在 UI Manager thread 上預處理,再在 main thread 上渲染上屏。
相關文章
相關標籤/搜索