React Native 分析(三)通信机制

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

    }
}
image.png

其实上面就是生产消息,等待 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的线程工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容