React Native 的通信,总体来说如下:
在启动阶段,初始化JS引擎,生成Native端模块配置表存于两端,其中模块配置是同步取得,而各模块的方法配置在该方法被真正调用时懒加载。
Native和JS端分别有一个bridge,发生调用时,调用端bridge查找模块配置表将调用转换成{moduleID, methodID, args(callbackID)},处理端通过同一份模块配置表转换为实际的方法实现。
Native->JS,原理上使用JSCore从Native执行JS代码,React-Native在此基础上给我们提供了通知发送的执行方式。
JS->Native,原理上JS并不主动调用Native,而是把方法和参数(回调)缓存到队列中,在Native事件触发并访问JS后,通过blocks回调Native。
这些涉及通信的线程,最好设计成一个用来专事专做的独立线程,这里也就是 JS 通信线程。如果通信内容涉及到更新 UI,则切换到主线程。其实多个 RootView 可以共享一个通信线程。
每一个Module都会创建一个自己独有的专属的串行GCD queue,每次js抛出来的各个module的通信,都是dispatch_async,不一定从哪个线程抛出来,但可以保证每个module内的通信事件是串行顺序的
如果不通过 methodQueue 方法设定具体的执行队列(dispatch_queue_t),则系统会自动创建一个默认线程,线程名称为 ModuleNameQueue;
对同类别组件进行划分,采用相同的执行队列(比如系统 UI 组件都是在 RCTUIManagerQueue 中执行)。这样有两点好处,一是为了控制组件执行队列的无序生长,二也可以控制特殊情况下的线程并发数。
下面我们分两个类型说明:
一、事件处理逻辑
我们知道,JS 本身只是负责组织界面逻辑,逻辑处理完以后,需要转换成 native 的渲染逻辑。
在16ms 中收集你做了哪些事,看看这些事情会不会对界面造成影响,如果有影响,则重新渲染。
那么是不是 native 每触发一个事件都会立即通知 JS端呢?
拿点击事件流程举例:
1.把 native 的 touch 事件转换封装成reactTouch。native 的 touch 事件表明了是哪个控件被点击,加上对应的 tag
UITouch *nativeTouch = _nativeTouches[touchIndex];
CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window];
CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:self.view];
UIView *touchView = _touchViews[touchIndex];
CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView];
NSMutableDictionary *reactTouch = _reactTouches[touchIndex];
reactTouch[@"pageX"] = @(rootViewLocation.x);
reactTouch[@"pageY"] = @(rootViewLocation.y);
reactTouch[@"locationX"] = @(touchViewLocation.x);
reactTouch[@"locationY"] = @(touchViewLocation.y);
reactTouch[@"timestamp"] = @(nativeTouch.timestamp * 1000); // in ms, for JS
2.发送reactTouch 事件
- (void)_updateAndDispatchTouches:(NSSet<UITouch *> *)touches
eventName:(NSString *)eventName
{
....
RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
reactTag:self.view.reactTag
reactTouches:reactTouches
changedIndexes:changedIndexes
coalescingKey:_coalescingKey];
....
[_eventDispatcher sendEvent:event];
}
我们来看下 send event 的注释,表明了事件会马上通知 JS。
/**
* Send a pre-prepared event object.
*
* Events are sent to JS as soon as the thread is free to process them.
* If an event can be coalesced and there is another compatible event waiting, the coalescing will happen immediately.
*/
- (void)sendEvent:(id<RCTEvent>)event;
那么如果Native同时有大量事件发生的话,是不是 JS 会频繁调用 Native 呢?有没有这个必要呢?
答案是否。JS 会采用批处理的方式处理,每隔5ms如果有事件发生,那么会把这5ms 内的事件集中起来一次性发送给 Native 执行。5ms这应该是经过一定实验得出的值。因为通信本身也是有一定消耗的。
3.转换成 JS 事件,通过 bridge 发送
// js thread only (which suprisingly can be the main thread, depends on used JS executor)
- (void)flushEventsQueue
{
[_eventQueueLock lock];
NSDictionary *events = _events;
_events = [NSMutableDictionary new];
NSMutableArray *eventQueue = _eventQueue;
_eventQueue = [NSMutableArray new];
_eventsDispatchScheduled = NO;
[_eventQueueLock unlock];
for (NSNumber *eventId in eventQueue) {
[self dispatchEvent:events[eventId]];
}
}
- (void)dispatchEvent:(id<RCTEvent>)event
{
[self enqueueJSCall:module method:method args:args completion:NULL];
}
最终传递给JS 的信息对象为
module:"RCTEventEmitter"
method:"receiveTouches"
args:
{
identifier = 1;
locationX = 48;
locationY = 21;
pageX = "199.5";
pageY = "366.5";
target = 13;
timestamp = "1043830468.345034";
}
Native 通知 JS
- (void)_callFunctionOnModule:(NSString *)module
method:(NSString *)method
arguments:(NSArray *)args
returnValue:(BOOL)returnValue
unwrapResult:(BOOL)unwrapResult
callback:(RCTJavaScriptCallback)onComplete
{
NSString *bridgeMethod = returnValue ? @"callFunctionReturnFlushedQueue" : @"callFunctionReturnResultAndFlushedQueue";
[self _executeJSCall:bridgeMethod arguments:@[module, method, args] unwrapResult:unwrapResult callback:onComplete];
}
接下来就是走 JS 方法进行业务逻辑运算,然后把结果通知给 Native 进行界面渲染。
如果JS多个事件发生间隔之间小于5ms,则先放入列队,等待批量处理。注意,如果你采用的是Remote JS Debugging 模式,则global.nativeFlushQueueImmediate始终是 undefined,因为这时候 js 运行在 chrome 上,不是 webview。这时候用的就是远程 websockets 通信,用一个新的 runloop 监听 sockets 事件。使用和不使用Remote JS Debugging,底层逻辑走的是不一样的。
enqueueNativeCall(moduleID: number, methodID: number, params: Array < any >, onFail: ?Function, onSucc: ?Function) {
const now = new Date().getTime();
if (global.nativeFlushQueueImmediate &&
(now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS ||
this._inCall === 0)) {
var queue = this._queue;
this._queue = [[], [], [], this._callID];
this._lastFlush = now;
global.nativeFlushQueueImmediate(queue);
}
}
其实上面就是生产消息,等待 Native flushQueue来消费。
二、界面渲染逻辑
RN 解决的是用JS来组织 Native 界面的问题,界面的渲染原理上一节已经和 VSync 一起说明了,以及对应的CADisplayLink 计时器,我们可以看到使用观察者模式,调用关心这个事件的对象的didUpdateFrame方法。
这个对象主要用于一些Timer,Navigator的Module需要按着屏幕渲染频率回调JS用的,只是给部分Module需求使用。
在各自模块的didUpdateFrame方法内,会有自己的业务逻辑,以DisplayLink的频率,主动call js
比如:RCTTimer模块
如果发起调用方OC,并不是在JSThread执行,RCTJSExecutor就会把代码perform到JSThread去执行
发起调用方是JS的话,所有JS都是在JSThread执行,所以handleBuffer也是在JSThread执行,只是在最终分发给各个module的时候,才进行了async+queue的控制分发。
- (void)_jsThreadUpdate:(CADisplayLink *)displayLink
{
RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink];
for (RCTModuleData *moduleData in _frameUpdateObservers) {
id<RCTFrameUpdateObserver> observer = (id<RCTFrameUpdateObserver>)moduleData.instance;
if (!observer.paused) {
RCTProfileBeginFlowEvent();
[self dispatchBlock:^{
RCTProfileEndFlowEvent();
[observer didUpdateFrame:frameUpdate];
} queue:moduleData.methodQueue];
}
}
[self updateJSDisplayLinkState];
}
iOS本身会使用这个计时器更新那些被标记为 dirty 的iOS对象,重新计算渲染。哪些对象被标记为 dirty 是由 React 的逻辑决定的。
详细情况上一节已经说明,这里就不再重复介绍。
那么怎么知道哪些对module 对js 公开了,并且公开了哪些方法呢?由以下几个步骤:
1.找出所有要导出给 JS 使用的 Module
2.把参数名和方法名序列化成 json对象
{
"remoteModuleConfig": [
[
"RCTFileRequestHandler"
],
[
"RCTDataRequestHandler"
],
...
]
}
3.把该 json 对象注入到 js 里
通过RCTJSExecutor,把这个json注入JSContext,在JS的global全局变量里面加入一个__fbBatchedBridgeConfig对象,是一个数组,里面记录着所有APIModule的name,这样相当于告知了JS,OC这边有多少个APIModule分别都叫做什么,可以被JS调用,但此时还没有告诉JS,每一个APIModule,都可以使用哪些方法。
4.JS主动调用nativeRequireModuleConfig这个 natvie方法,找OC一一确认每一个APIModule里面都有啥具体信息,具体Method方法。
通过名字,找到对应的RCTModuleData,从而调用RCTModuleData的config方法
- (NSArray *)config
{
...
for (id<RCTBridgeMethod> method in self.methods) {
if (method.functionType == RCTFunctionTypePromise) {
if (!promiseMethods) {
promiseMethods = [NSMutableArray new];
}
[promiseMethods addObject:@(methods.count)];
}
else if (method.functionType == RCTFunctionTypeSync) {
if (!syncMethods) {
syncMethods = [NSMutableArray new];
}
[syncMethods addObject:@(methods.count)];
}
[methods addObject:method.JSMethodName];
}
NSArray *config = @[
self.name,
RCTNullIfNil(constants),
RCTNullIfNil(methods),
RCTNullIfNil(promiseMethods),
RCTNullIfNil(syncMethods)
];
...
}
会调用RCTModuleData的methods方法拿到一个所有方法的数组
循环找到以rct_export开头的方法
从这个方法中得到字符串nativeAlert:(NSString *)content xxxx
截取 : 前面的字符串nativeAlert作为JS简写方法名
有类名+类的简写方法名数组,生成APIModule的info信息,转换成json,返回给JS
["VKAlertModule",["nativeAlert"]]
这样JS就完全知晓,Native所有APIModule的名字,每个APIModule下所有的Method的名字了。
RCTBatchedBridge的executeSourceCode方法
RCTDisplayLink的addToRunLoop方法
至此GCD group内所有的基础工作都已完成,loadjs完毕,配置module完毕,配置JSExecutor完毕,可以放心的执行JS代码了来运行界面和业务逻辑了。
首先要加载生成的jsbundle,通过[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:]来在JSExecutor专属的Thread内执行jsbundle代码
最早创建的RCTDisplayLink一直都只是创建完毕,但并没有运作,此时把这个displaylink绑在JSExecutor的Thread所在的runloop上,这样displaylink开始运作
整个RN在通信建立在bridge上面,各种GCD,线程,队列,displaylink,还是挺复杂的,针对各个module也都是有不同的处理,把这块梳理清楚能让我们更加清楚OC代码里面,RN的线程控制,更方便以后我们扩展编写更复杂的module模块,处理更多native的线程工作。