本文通过分析 RN 源码,简要介绍了 JS to Native 的 callback 实现原理以及 RN 中的三个重要线程。
©原创文章,转载请注明出处!
callback
前两篇文章ReactNative源码解析——通信机制详解(1/2) 、ReactNative源码解析——通信机制详解(2/2)分别介绍了 RN 通信机制中的 JS to Native、Native to JS 的执行流程。为了集中注意力抓住主要流程,当时没有分析调用过程中的 callback 问题,下面简要分析一下 JS to Native callback 的实现原理。
文中所列代码均做了简化处理。
首先,来看一个具体的例子:
1 | RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options |
showShareActionSheetWithOptions:failureCallback:successCallback:
是RCTActionSheetManager
曝露给 JS 的方法之一,其包含两个 callback:failureCallback
、successCallback
。
其中,RCTResponseErrorBlock
、RCTResponseSenderBlock
的定义如下:
1 | /** |
JS 中的调用:
1 | ActionSheetIOS.showShareActionSheetWithOptions({ |
上述代码的第7
行、8~16
行,分别设置了两个 fail、success callback。
上图所示是 JS to Native 中最终在 Native 侧调用相应方法的调用栈,在ReactNative源码解析——通信机制详解(1/2)
中提到过,但限于篇幅没有展开讨论。
今天的分析就从RCTModuleMethod#invokeWithBridge:module:arguments:
开始。
RCTModuleMethod#invokeWithBridge:
1 | - (id)invokeWithBridge:(RCTBridge *)bridge |
在invokeWithBridge:module:arguments:
内部(第6
行)调用了processMethodSignature
方法,从名称可知该方法是处理『方法签名』的(被处理的方法当然是被 JS 调用的 Native method 了):
1 | - (void)processMethodSignature |
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(如:RCTResponseSenderBlock
、RCTResponseErrorBlock
等)(第35~46
行),则在argumentBlock
中生成一个 bolck,用作调用方法时的实参(因为 block 类型无法从 JS 直接传给 Native),在该 block 中调用了bridge
的enqueueCallback:args:
方法。
再回到RCTModuleMethod#invokeWithBridge:module:arguments:
方法,第12~18
行,执行了processMethodSignature
方法为每个参数生成的 block,最终的效果就是将 JS 侧传入的参数值转换成 Native 类型并设置到invocation
上。
通过上述分析可知,对于 block 类型的参数(callback)最终会调用了RCTCxxBridge#enqueueCallback:args:
方法。
1 | void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) { |
沿调用链最终来到JSCExecutor::invokeCallback
,第4
行通过 hook,实际调用的是 JS 侧的MessageQueue#invokeCallbackAndReturnFlushedQueue
方法。
下面我们来看看 JS 侧如何处理 callback。
NativeModules#genMethod
还记得genMethod
方法吗?
1 | function genMethod(moduleID: number, methodID: number, type: MethodType) { |
在ReactNative源码解析——通信机制详解(1/2)
一文中介绍过,JS 在调用 Native 方法时,会在 JS 侧动态生成一个对应的 JS 方法。
在genMethod
方法的第4~9
行,处理了调用参数的最后2个,判断是否是 callback 类型(function 类型)。
从上述处理代码中可以得出:
- 对于 callback 类型的参数,作了特殊处理,将其从参数列表中剥离出来;
- 一个方法最多只能有两个 callback 类型的参数;
- callback 类型的参数只能位于参数列表的最后。
下面再来看看MessageQueue.enqueueNativeCall
:
1 | enqueueNativeCall( |
可以看到:
- 对于 callback 类型的参数,在 JS 与 Native 间传递的是 callbackID(
_successCallbacks
、_failureCallbacks
中的下标); - JS 侧的 callback function 存储在
_successCallbacks
、_failureCallbacks
中。
我们再回到调用流程中的MessageQueue#invokeCallbackAndReturnFlushedQueue
方法,在该方法中调用了__invokeCallback
方法:
1 | __invokeCallback(cbID: number, args: any[]) { |
在__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
方法中:
1 | _jsThread = [[NSThread alloc] initWithTarget:[self class] |
在阅读 RN 源码时可能会发现RCTMessageThread
类,它是对 JS Thread 的 C++封装。具体源码就不列了。
还会发现RCTJSThread
变量:
1 | dispatch_queue_t RCTJSThread; |
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
方法,会如何?
1 | - (void)setUpMethodQueue |
在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。
1 | dispatch_queue_t RCTGetUIManagerQueue(void) |
可以看到,UI Manager Thread 是一个高优先级的串行队列。
小结
- 所有 JS 代码都会在独立线程 JSThread 上执行;
- 可通过 methodQueue 方法自定义 Native module 执行线程;
- 为了提高效率,所有 UI 组件都会在 UI Manager thread 上预处理,再在 main thread 上渲染上屏。
Gitalking ...