[深入ReactNative] 通讯及消息循环代码剖析

肥皂V 2016 1.3
http://www.jianshu.com/p/269b21958030

React Native 已经推出近一年时间了,近期也在研究iOS下用js写app的框架,从徘徊和犹豫中,最终还是选定React Native,她就像若隐若现的女神一样,想要下决心追到,可是不容易。要想把她应用的已存在的有一定体量的app中,更是不易,让我先把她里外都了解清楚,在分享一下合理应用到现有app的方案,这是深入React Native系列的第一篇,后续会继续分享使用过程中的一些认识。

本篇详细分析下React Native 中 Native和JS的互相调用的原理解析。之前bang的文章已经介绍过,本文从代码层面更深入的来讲解,
分析基于 React Native 0.17.0 版本, RN在快速进化,其中的内容已和之前的旧版本有些不同

作为初篇,先创建一个示例工程,以后的分享都以这个工程为基础。目前这个工程还很简单,main.js的讲解可以下载这里的代码

GitHub MGReactNativeTest
工程里有直接改动main.jsbundle

示例工程的代码

  render: function() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <Text style={styles.instructions}>
          {this.state.changeText}
        </Text>
        <Text style={styles.welcome} onPress={this._onPress}>
          Change
        </Text>
      </View>
    );
  },
11.png

)

Native 与 JS的互相调用

0.17版本的React Native JS引擎已经全部使用的是iOS自带的JavaScriptCore,在JSContext提供的Native与js互相调用的基础上,封装出了自己的互调方法。下面是一张结构图

架构图.png

App启动过程中 Native和JS互相调用的日志

[Log] N->JS : RCTDeviceEventEmitter.emit(["appStateDidChange",{"app_state":"active"}]) (main.js, line 638)
[Log] N->JS : RCTDeviceEventEmitter.emit(["networkStatusDidChange",{"network_info":"wifi"}]) (main.js, line 638)
[Log] N->JS : AppRegistry.runApplication(["MGReactNative",{"rootTag":1,"initialProps":{}}]) (main.js, line 638)
[Log] Running application "MGReactNative" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([2,"RCTView",1,{"flex":1}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([3,"RCTView",1,{"flex":1}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([4,"RCTView",1,{"flex":1,"justifyContent":"center","alignItems":"center","backgroundColor":4294311167}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([5,"RCTText",1,{"fontSize":20,"textAlign":"center","margin":10,"accessible":true,"allowFontScaling":true}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([6,"RCTRawText",1,{"text":"Welcome to React Native!"}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.manageChildren([5,null,null,[6],[0],null]) (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([7,"RCTText",1,{"textAlign":"center","color":4281545523,"marginBottom":5,"accessible":true,"allowFontScaling":true}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([8,"RCTRawText",1,{"text":"soap1"}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.manageChildren([7,null,null,[8],[0],null]) (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([9,"RCTText",1,{"fontSize":20,"textAlign":"center","margin":10,"accessible":true,"allowFontScaling":true,"isHighlighted":false}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([10,"RCTRawText",1,{"text":"Change"}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.manageChildren([9,null,null,[10],[0],null]) (main.js, line 638)
[Log] JS->N : RCTUIManager.manageChildren([4,null,null,[5,7,9],[0,1,2],null]) (main.js, line 638)
[Log] JS->N : RCTUIManager.manageChildren([3,null,null,[4],[0],null]) (main.js, line 638)
[Log] JS->N : RCTUIManager.createView([12,"RCTView",1,{"position":"absolute"}]) (main.js, line 638)
[Log] JS->N : RCTUIManager.manageChildren([2,null,null,[3,12],[0,1],null]) (main.js, line 638)
[Log] JS->N : RCTUIManager.manageChildren([1,null,null,[2],[0],null]) (main.js, line 638)

日志显示了启动React Native 界面 Native与JS的调用过程,我们从最简单的例子入手,慢慢脱下女神的面纱。

Native调用JS (Native->JS)

可以看到,启动开始之后,Native调用了JS的 RCTDeviceEventEmitter.emit 广播了两个事件 appStateDidChange,networkStatusDidChange
随后调用 AppRegistry.runApplication(["MGReactNative",{"rootTag":1,"initialProps":{}}]) 启动了React Native引擎。
下面我们一点点分析,是如果从Native调用到JS的函数AppRegistry.runApplication的

系统JavascriptCore 中Native如何调用JS

JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"function add(a, b) { return a + b; }"];
JSValue *add = context[@"add"];
NSLog(@"Func:  %@", add);
 
JSValue *sum = [add callWithArguments:@[@(7), @(21)]];
NSLog(@"Sum:  %d",[sum toInt32]);
//OutPut:
//  Func:  function add(a, b) { return a + b; }
//  Sum:  28

JSContext 是运行 JavaScript 代码的环境。一个 JSContext 是一个全局环境的实例,我们可以从 JSContext全局变量中用下标的方式取出JS代码中定义的函数 add,它用JSValue类型包装了一个 JS 函数, 如果你确定JSValue是一个JS函数类型,可以使用callWithArguments 来调用它。
更详细的介绍可以学习这篇文章 Java​Script​Core

AppRegistry.runApplication

聪明的你一定想到,React Native 的也是用同样方式调用到AppRegistry.runApplication,是的,不过是通过一个通用接口来调用的
RCTJavaScriptContext 封装了OC方法callFunction,

- (void)callFunctionOnModule:(NSString *)module
                      method:(NSString *)method
                   arguments:(NSArray *)args
                    callback:(RCTJavaScriptCallback)onComplete
{
  // TODO: Make this function handle first class instead of dynamically dispatching it. #9317773
  [self _executeJSCall:@"callFunctionReturnFlushedQueue" arguments:@[module, method, args] callback:onComplete];
}

_executeJSCall 执行的具体代码是

    method =  @“callFunctionReturnFlushedQueue”
    JSStringRef moduleNameJSStringRef = JSStringCreateWithUTF8CString("__fbBatchedBridge");
    JSValueRef moduleJSRef = JSObjectGetProperty(contextJSRef, globalObjectJSRef, moduleNameJSStringRef, &errorJSRef);
    JSStringRef methodNameJSStringRef = JSStringCreateWithCFString((__bridge CFStringRef)method);
    JSValueRef methodJSRef = JSObjectGetProperty(contextJSRef, (JSObjectRef)moduleJSRef, methodNameJSStringRef, 
    resultJSRef = JSObjectCallAsFunction(contextJSRef, (JSObjectRef)methodJSRef, (JSObjectRef)moduleJSRef, 0, NULL, 

可以看到Native 从JSContext中拿出JS全局对象 __fbBatchedBridge,然后调用了其callFunctionReturnFlushedQueue函数

是时候克服心中的恐惧,开始掀裙子了 ,来看看main.jsbundle中的JS代码

上文Natvie调用JS的路径到了 __fbBatchedBridge.callFunctionReturnFlushedQueue js代码这一步,demo工程中,我们自己写的index.ios.js 只有区区几行,去Node Server转一圈或React Native的预编译之后,竟然产生了1.3M,近5W行JS代码的main.jsbundle 文件,对于终端同学来说,简直是一座五指山。不要害怕,我们一起探寻其中的奥妙。

继续跟踪js代码中的 __fbBatchedBridge

//main.js
__d('BatchedBridge',function(global, require, module, exports) {  'use strict';
    var MessageQueue=require('MessageQueue');
    var BatchedBridge=new MessageQueue(
        __fbBatchedBridgeConfig.remoteModuleConfig,
        __fbBatchedBridgeConfig.localModulesConfig);

    Object.defineProperty(global,'__fbBatchedBridge',{value:BatchedBridge});

    module.exports=BatchedBridge;
});

我们发现这段JS代码中有这句 Object.defineProperty(global,'__fbBatchedBridge',{value:BatchedBridge});

准备知识,对JS很熟的同学可以略过或指正

这段JS代码怎么理解呢,这个是nodejs的模块代码,当打包成main.js之后,含义又有变化,我们简单可以这样理解,__d() 是一个定义module的JS函数,其就等于下面这段代码中的 define 函数,从代码上很容易可以理解,它定义一个module,名字Id为BatchedBridge,同时传递了一个工厂函数,另一个模块的代码可以通过调用require获取这个module,例如
var BatchedBridge=require('BatchedBridge');

这是一个懒加载机制,当有人调用require时,工厂函数才执行,在代码最后,把这个模块要导出的内容赋值给module.exports。

        function define(id,factory){
            modules[id]={
                factory:factory,
                module:{exports:{}},
                isInitialized:false,
                hasError:false};}
                
        function require(id){
            var mod=modules[id];
            if(mod&&mod.isInitialized){
                return mod.module.exports;}

好,我们抓紧回来,在上段代码中当BatchedBridge module创建时,通过这句 Object.defineProperty(global,'__fbBatchedBridge',{value:BatchedBridge}); 把自己定义到JSContext的全局变量上。所以在Native代码中可以通过 JSContext[@"__fbBatchedBridge"]获取到,

从代码中也可以看到BatchedBridge 是JS类MessageQueue的实例,并且它导出的时候并没有导出构造函数MessageQueue,而是导出的实例BatchedBridge,所以它是React Native JS引擎中全局唯一的。它也是Natvie和JS互通的关键桥梁。

 __fbBatchedBridge.callFunctionReturnFlushedQueue("AppRegistr","runApplication",["MGReactNative",{"rootTag":1,"initialProps":{}}])

我们继续看MessageQueue 类的callFunctionReturnFlushedQueue 函数,它最终调用到__callFunction(module, method, args)函数

__callFunction(module, method, args) {
    var moduleMethods = this._callableModules[module];
    if (!moduleMethods) {

      moduleMethods = require(module);
    }
    
    moduleMethods[method].apply(moduleMethods, args);
  }

看起来__callFunction就是最终的分发函数了,首先它从this._callableModules中找到模块对象,如果它还没有加载,就动态加载它(require),如果找到就执行最终的JS函数。

自己开发的JS模块如果暴露给Native调用

先看下AppRegistry是如何暴露给Natvie的


__d('AppRegistry',function(global, require, module, exports) {  'use strict';

    var BatchedBridge=require('BatchedBridge');
    var ReactNative=require('ReactNative');

    var AppRegistry={

        runApplication:function(appKey,appParameters){
            runnables[appKey].run(appParameters);
            },
        }

    BatchedBridge.registerCallableModule(
        'AppRegistry',
        AppRegistry);

    module.exports=AppRegistry;
});

有前面的讲解,现在看这个应该不态费劲了,可以看到AppRegistry模块工厂函数中,执行了 BatchedBridge.registerCallableModule('AppRegistry',AppRegistry);,把自己注册到BatchedBridge的CallableModule中,所以在上一节中,__callFunction才能在_callableModules找到AppRegistry实例,才能调用其runApplication函数。自己写的模块代码可以用React Native这种方式暴露给Natvie调用,和直接暴露的区别是,符合React Natvie的模块化原则,另外一个直观的好处是你的模块可以是懒加载的,并且不会污染全局空间。

目前终于把从N-JS的整个路径跑通了,我们梳理下整个流程看看。

1 [RCTBatchedBridge enqueueJSCall:@“AppRegistry.runApplication” args:["MGReactNative",{"rootTag":1,"initialProps":{}}]];

2 RCTJavaScriptContext callFunctionOnModule:@"AppRegistr"
                      method:@"runApplication"
                   arguments:["MGReactNative",{"rootTag":1,"initialProps":{}}]
                    callback:(RCTJavaScriptCallback)onComplete

//main.js
3  __fbBatchedBridge.callFunctionReturnFlushedQueue("AppRegistr","runApplication",["MGReactNative",{"rootTag":1,"initialProps":{}}])

//main.js
4  BatchedBridge.__callFunction("AppRegistr","runApplication",["MGReactNative",{"rootTag":1,"initialProps":{}}])

//main.js
5 var moduleMethods = BatchedBridge._callableModules[module];
    if (!moduleMethods) {
      moduleMethods = require(module);
    }
    moduleMethods[method].apply(moduleMethods, args);

JS调用Native (JS->Native)

接下来我们看看从JS如何调用Native,换句话说Native如何开放API给JS

我们以弹Alert框的接口为例,这是Native的OC代码,导出RCTAlertManager类的alertWithArgs:(NSDictionary *)args
callback:(RCTResponseSenderBlock)callback)方法

@interface RCTAlertManager() : NSObject <RCTBridgeModule, RCTInvalidating>
...
@end

@implementation RCTAlertManager
RCT_EXPORT_MODULE()

RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
                  callback:(RCTResponseSenderBlock)callback)
{
...
}
#end

要把OC类或实例的函数导出给JS用,需实现以下三个步骤

  • OC类实现RCTBridgeModule协议
  • 在.m的类实现中加入RCT_EXPORT_MODULE(),帮助你实现RCTBridgeModule协议
  • 要导出的函数用RCT_EXPORT_METHOD()宏括起来,不用这个宏,不会导出任何函数

现在从JS里可以这样调用这个方法:

var RCTAlertManager=require('react-native').NativeModules.AlertManager;
RCTAlertManager.alertWithArgs({message:'JS->Native Call',buttons:[{k1:'button1'},{k2:'button1'}]},function(id,v) 
{console.log('RCTAlertManager.alertWithArgs() id:' + id +' v:' + v)});

执行之后的效果,弹出一个Alert

alert.png

对于详细的如何导出函数推荐阅读Native Modules

我们今天的目的不是和女神喝茶聊天,是深入女神内心,是内心咳咳。来看看今天的重点

动态导出Native API,延迟加载Native 模块

在JS中可以直接使用RCTAlertManager.alertWithArgs来调用,说明JS中已经定义了和OC对象相对应的JS对象,我们从导出一个Native类开始,完整跟踪下这个过程。

生成Native API 配置表

RCTAlertManager类实现了RCTBridgeModule协议,并且在类的实现里包含了RCT_EXPORT_MODULE() 宏

@protocol RCTBridgeModule <NSObject>

 #define RCT_EXPORT_MODULE(js_name) \
RCT_EXTERN void RCTRegisterModule(Class); \
+ (NSString *)moduleName { return @#js_name; } \
+ (void)load { RCTRegisterModule(self); }

// Implemented by RCT_EXPORT_MODULE
+ (NSString *)moduleName;

@optional

在OC里,一个类所在文件被引用时,系统会调用其+(void)load函数,当RCTAlertManager所在文件被引用时,系统调用load 函数,函数里简单的调用RCTRegisterModule(self) 把自己注册到一个全局数组RCTModuleClasses,这样系统中导出的类都会自动注册到这个全局变量数组里(so easy)。

在JS中有一个BatchedBridge用来和Native通讯,在Natvie中也有一个RCTBatchedBridge类,它封装了JSContext即JS引擎
在RCTBatchedBridge start 函数中,做了5件事

  1. jsbundle文件的下载或本地读取(异步)
  2. 初始化导出给JS用的Native模块
  3. 初始化JS引擎
  4. 生成配置表,并注入到JS引擎中,
  5. 执行jsbundle文件。
 //伪代码
 - (void)start
{
  //1  jsbundle文件的下载或本地读取(异步)
  NSData *sourceCode;
  [self loadSource:^(NSError *error, NSData *source) {sourceCode = source}];

  //2 初始化导出给JS用的Native模块
  [self initModules];

  //3 初始化JS引擎
  [self setUpExecutor];

  //4 生成Native模块配置表 把配置表注入到JS引擎中
  NSSting* config = [self moduleConfig];
  [self injectJSONConfiguration:config onComplete:^(NSError *error) {});
  
  //5 最后执行jsbundle
  [self executeSourceCode:sourceCode];

}

现在我们最关心第二步初始化Native模块 initModules 和moduleConfig 到底是什么

//伪代码
- (void)initModules
{
  //遍历上节讲到的RCTGetModuleClasses全局数组,用导出模块的类或者实例创建RCTModuleData
  for (Class moduleClass in RCTGetModuleClasses()) 
  {
    NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass);
    
    //这里一个很有意思的地方,如果导出的类或其任何父类重写了init方法,或者类中有setBridge方法
    //则React Native假设开发者期望这个导出模块在Bridge第一次初始化时实例化,否则怎么样,大家想想
    if ([moduleClass instanceMethodForSelector:@selector(init)] != objectInitMethod ||
        [moduleClass instancesRespondToSelector:setBridgeSelector]) {
      module = [moduleClass new];
    }

    // 创建RCTModuleData
    RCTModuleData *moduleData;
    if (module) {
        moduleData = [[RCTModuleData alloc] initWithModuleInstance:module];
    } else {
       moduleData = [[RCTModuleData alloc] initWithModuleClass:moduleClass                                                   bridge:self];
    }

     //保存到数组中,数组index就是这个模块的索引
     [_moduleDataByID addObject:moduleData];
  }
}

initModules里根据是否重写init或添加了setBridge来决定是不是要马上实例化RCTGetModuleClasses里的导出类,然后用实例或类创建RCTModuleData,缓存到本地,以便JS调用时查询。

再来看第四步导出的 NSSting* config = [self moduleConfig] 是什么内容

    {"remoteModuleConfig":
    [["RCTStatusBarManager"],
    ["RCTSourceCode"],
    ["RCTAlertManager"],
    ["RCTExceptionsManager"],
    ["RCTDevMenu"],
    ["RCTKeyboardObserver"],
    ["RCTAsyncLocalStorage"],
    .
    .
    .
    ]}  

它仅仅是一个类名数组。

注入配置表到JS引擎,并创建对应的JS对象

生产配置表后,通过下面的方法把这个类名数组注入到JSContext,赋值给JS全局变量__fbBatchedBridgeConfig

    [_javaScriptExecutor injectJSONText:configJSON
                  asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
                             callback:onComplete];

在JS端,当有人引用了BatchedBridge var BatchedBridge=require('BatchedBridge');,其工厂函数会通过 __fbBatchedBridgeConfig配置表创建MessageQueue的实例BatchedBridge

    var MessageQueue=require('MessageQueue');

    var BatchedBridge=new MessageQueue(
        __fbBatchedBridgeConfig.remoteModuleConfig,
        __fbBatchedBridgeConfig.localModulesConfig);

我们看看MessageQueue的构造函数,构造函数里为每个导出类创建了一个对应的module对象,因为此时config里只有一个导出类的名字,所以这里只为这个对象增加了一个成员变量 module.moduleID,并把module保存到this.RemoteModules数组里

  _genModule(config, moduleID) {
    let module = {};
    if (!constants && !methods && !asyncMethods) {
      module.moduleID = moduleID;
    }
    this.RemoteModules[moduleName] = module;
  }

接着我们顺藤摸瓜看看那里使用的BatchedBridge.RemoteModules

NativeModules模块

NativeModules在初始化时,用BatchedBridge.RemoteModules保存的类名列表,为每个JS对象增加了函数等属性

__d('NativeModules',function(global, require, module, exports) {  'use strict';
    var RemoteModules=require('BatchedBridge').RemoteModules;
    var NativeModules={};
    
     //遍历NativeModules中导出类名
    Object.keys(RemoteModules).forEach(function(moduleName){
    
       //把类名定义为NativeModules的一个属性,比如AlertManager类,定义只有就可以用NativeModules.AlertManager 访问
        Object.defineProperty(NativeModules,moduleName,{
        
            //这个属性(AlertManager)是可以遍历的,当然属性也是个对象里面有属性和函数
            enumerable:true,
            
            //属性都有get和set函数,当调用访问这个属性时,会调用get函数  NativeModules.AlertManager         
            get:function(){

                var module=RemoteModules[moduleName];
                if(module&&typeof module.moduleID==='number'&&global.nativeRequireModuleConfig){
                
                    //调用Native提供的全局函数nativeRequireModuleConfig查询AlertManager 导出的常量和函数
                    var json=global.nativeRequireModuleConfig(moduleName);                                   
                    module=config&&BatchedBridge.processModuleConfig(JSON.parse(json),module.moduleID);
                    RemoteModules[moduleName]=module;
                }
                return module;
            }
        });
    });
    module.exports=NativeModules;
});

React Native 把所有的Native导出类定义在一个NativeModules模块里,所以使用Natvie接口时也可以直接这样拿到对应的JS对象
var RCTAlertManager=require('NativeModules').AlertManager;

代码里我加了注释
思考一个问题,为什么React Natvie搞的那么麻烦,为什么不在上一个步骤里(MessageQueue的构造函数)里就创建出完整的JS对象。

没错,就是模块的懒加载,虽然Native导出了Alert接口,在JS引擎初始化后,JS里只存在一个名字为AlertManager的空对象

906C7A90-0A85-4FD6-B433-39CE041D4445.png

当调用了RCTAlertManager.alertWithArgs({message:'JS->Native Call',buttons:[{k1:'button1'}时,才会调用AlertManager 的get函数到Native里查询导出的常量和函数,并定义到AlertManager中。

7RGT1@Z}N19_9{KQ~P_SDFE.jpg

Native模块对应的JS对象中函数是如何调用到Native

RCTAlertManager.alertWithArgs 这个函数是如何调用到Native里的呢,在BatchedBridge.processModuleConfig函数中,用_genMethod创建了一个闭包fn为每个函数赋值,这个函数转调self.__nativeCall(module, method, args, onFail, onSucc); 我们调用RCTAlertManager.alertWithArgs函数,其实都是调用的这个fn闭包。

    _genMethod(module, method, type) {
      fn = function(...args) {
        return self.__nativeCall(module, method, args, onFail, onSucc);
      };
    return fn;
  }

__nativeCall,好熟悉的名字,

  __nativeCall(module, method, params, onFail, onSucc) {

    this._queue[MODULE_IDS].push(module);
    this._queue[METHOD_IDS].push(method);
    this._queue[PARAMS].push(params);

    global.nativeFlushQueueImmediate(this._queue);
    this._queue = [[],[],[]];
    this._lastFlush = now;

  }

global.nativeFlushQueueImmediate 是Native提供的接口,__nativeCall把需要调用的module,method,params都塞到队列里,然后传递到Native,

我们在回到Native 找到上文提到的两个关键接口,Native模块查询接口:global.nativeRequireModuleConfig和调用接口global.nativeFlushQueueImmediate,他们是在JS引擎(JSContext)初始化时,定义到全局变量的。

//RCTContextExecutor setUP
//简化过的代码
- (void)setUp
{
   ...
    self->_context.context[@"nativeRequireModuleConfig"] = ^NSString *(NSString *moduleName) {
      NSArray *config = [weakBridge configForModuleName:moduleName];
      return RCTJSONStringify(config, NULL);
    };

    self->_context.context[@"nativeFlushQueueImmediate"] = ^(NSArray<NSArray *> *calls){
      [weakBridge handleBuffer:calls batchEnded:NO];
    };
    ...
}

[weakBridge handleBuffer:calls batchEnded:NO]; 经过一系列传递,调用到_handleRequestNumber 中,用moduleID找到RCTModuleData,再用methodID 找到id<RCTBridgeMethod> method 然后在moduleData.instance实例中执行

- (BOOL)_handleRequestNumber:(NSUInteger)i
                    moduleID:(NSUInteger)moduleID
                    methodID:(NSUInteger)methodID
                      params:(NSArray *)params
{

  RCTModuleData *moduleData = _moduleDataByID[moduleID];
  id<RCTBridgeMethod> method = moduleData.methods[methodID];

  [method invokeWithBridge:self module:moduleData.instance arguments:params];
}

这里有必要再强调一次moduleData.instance 这个地方。

- (id<RCTBridgeModule>)instance
{
  if (!_instance) {
    _instance = [_moduleClass new];
    ...
  }
  return _instance;
}

还记的前面BatchedBridge 初始化时的initModules吗

    //这里一个很有意思的地方,如果导出的类或其任何父类重写了init方法,或者类中有setBridge方法
    //则React Native假设开发者期望这个导出模块在Bridge第一次初始化时实例化,否则怎么样,大家想想
    if ([moduleClass instanceMethodForSelector:@selector(init)] != objectInitMethod ||
        [moduleClass instancesRespondToSelector:setBridgeSelector]) {
      module = [moduleClass new];
    }

否则就是在用户真正调用时,在moduleData.instance里实例化,React Native已经懒到骨髓了。

RCTModuleData中每个函数的封装 RCTModuleMethod里还有一个优化点,JS传递到Native的参数需要进行响应的转换,RCTModuleMethod在调用函数只前,先预解析一下,创建每个参数转换的block,缓存起来,这样调用时,就直接使用对应函数指针进行参数转换了,大要详细了解可以看 - (void)processMethodSignature函数。

回调函数

前面我们为了直观,忽略了回调函数,alertWithArgs的第二个参数是一个JS回调函数,用来指示用户点击了哪个button,并打印出一行日志。

RCTAlertManager.alertWithArgs({message:'JS->Native Call',buttons:[{k1:'button1'},{k2:'button1'}]},function(id,v) 
{console.log('RCTAlertManager.alertWithArgs() id:' + id +' v:' + v)});

回调函数的调用和直接从Native调用JS是差不多的,再回头看看__nativeCall 函数我们忽略的部分

  __nativeCall(module, method, params, onFail, onSucc) {

    //Native接口最多支持两个回调
    if (onFail || onSucc) {
      onFail && params.push(this._callbackID);
      this._callbacks[this._callbackID++] = onFail;
      onSucc && params.push(this._callbackID);
      this._callbacks[this._callbackID++] = onSucc;
    }
    
    this._queue[MODULE_IDS].push(module);
    this._queue[METHOD_IDS].push(method);
    this._queue[PARAMS].push(params);

    global.nativeFlushQueueImmediate(this._queue);
    this._queue = [[],[],[]];
    this._lastFlush = now;

  }

可以看到把onFail,onSucc两个函数类型转化为两个数字ID插入到参数列表后面,并把函数函数缓存起来。
从Native调用过来也比较简单了,传递过callbackID到JS,就可以执行到回调函数。

JS传递的参数仅仅是个整形ID,Native如何知道这个ID就是个回调函数呢?

答案是和其他参数一样通过Native的函数签名,如果发现对应的参数是个block,则从JS传递过来的ID就是对应的回调ID,把其转化为RCTResponseSenderBlock的闭包。

RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args callback:(RCTResponseSenderBlock)callback)

到此为止,我们已经把整个JS->Natvie的流程都走通了,
梳理一下整个流程。

调用图.png

总结一下

  1. Native初始化时, Native生成要导出模块的名字列表(注意注意注意),仅仅是模块(类)名字列表, ModuleConfig
  1. 在React Native 的JS引擎初始化完成后,向JSContext注入ModuleConfig,赋值到JS全局变量 __fbBatchedBridgeConfig
  2. 还记得那个N->JS大使---JS对象BatchedBridge吗,BatchedBridge创建的时候会用__fbBatchedBridgeConfig变量里Native模块名字列表定义一个同名的JS对象,但是是一个没有任何方法的空对象,只增加了一个获取方法数组的get函数。此时初始化的操作已完成。
  3. 很久很久之后,有人用RCTAlertManager.alertWithArgs 调用了Native的代码,咳咳,这人是我,此时JS去获取RCTAlertManager方法列表时,发现是空的,就调用Native提供的查询函数nativeRequireModuleConfig 获取RCTAlertManager对象的详细的导出信息(方法列表),并定义成同名的JS函数,此函数转调到OC的实现
  4. 此时RCTAlertManager对应的JS对象才定义完整,JS找到了alertWithArgs函数,每个对应的JS函数都是一个封装了调用__nativeCall的闭包,JS通过此函数转发到Native

可以看出,Native导出的配置表并不是在一开始就完整的注入JS并定义对应的JS对象,而是仅仅注入了一个模块名字,当运行期间有人调用的时候,才再从Native查询调用模块的详细配置表,这种懒加载机制缓解了一个大型的app导出的Api很多,全部导入JS导致初始化时内存占用过大的问题。

消息循环

线程问题

React Native为JS引擎创建了一个独立的线程

//RCTJavaScriptContext
- (instancetype)init
{
  NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class]
                                                       selector:@selector(runRunLoopThread)
                                                         object:nil];
  javaScriptThread.name = @"com.facebook.React.JavaScript";
  [javaScriptThread start];
  return [self initWithJavaScriptThread:javaScriptThread context:nil];
}

所有的JS代码都运行在"com.facebook.React.JavaScript"后台线程中,所有的操作都是异步,不会卡死主线程UI。并且JS调用到Native中的接口中有强制的线程检查,如果不是在React线程中则抛出异常。
这样有一个问题,从JS调用Native中的代码是执行在这个后台线程中,我们上文的RCTAlertManager.alertWithArgs明显是个操作UI的接口,执行在后台线程会crash,在导出RCTAlertManager时,通过实现方法- (dispatch_queue_t)methodQueue,原生模块可以指定自己想在哪个队列中被执行

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}

类似的,如果一个操作需要花费很长时间,原生模块不应该阻塞住,而是应当声明一个用于执行操作的独立队列。举个例子,RCTAsyncLocalStorage模块创建了自己的一个queue,这样它在做一些较慢的磁盘操作的时候就不会阻塞住React本身的消息队列:

- (dispatch_queue_t)methodQueue
{
  return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL);
}

React的消息循环

这是典型的事件驱动机制和消息循环,当无任何事件时,runloop(消息循环)处于睡眠状态,当有事件时,比如用户操作,定时器到时,网络事件等等,触发一此消息循环,最总表现为UI的改变或数据的变化。

消息循环.png

这里要注意的是,以上我们讲到从 JS调用到Native是调用global.nativeFlushQueueImmediate 立即执行的。React消息循环这里做了一次缓存,比如用户点击一次,所有触发的JS->N的调用都缓存到MessageQueue里,当N->JS调用完成时,以返回值的形式返回MessageQueue, 减少了N->JS的交互次数。缓存时间是 MIN_TIME_BETWEEN_FLUSHES_MS = 5毫秒内的调用。

  __nativeCall(module, method, params, onFail, onSucc) {

    this._queue[MODULE_IDS].push(module);
    this._queue[METHOD_IDS].push(method);
    this._queue[PARAMS].push(params);

    var now = new Date().getTime();
    if (global.nativeFlushQueueImmediate &&
        now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS) {
      global.nativeFlushQueueImmediate(this._queue);
      this._queue = [[],[],[]];
      this._lastFlush = now;
    }

  }

MIN_TIME_BETWEEN_FLUSHES_MS时间内的调用都会缓存到this._queue,以返回值的形式返回给Native,形成一次消息循环

  callFunctionReturnFlushedQueue(module, method, args) {
    guard(() => {
      this.__callFunction(module, method, args);
      this.__callImmediates();
    });

    return this.flushedQueue();
  }


  flushedQueue() {
    this.__callImmediates();

    let queue = this._queue;
    this._queue = [[],[],[]];
    return queue[0].length ? queue : null;
  }

本篇的内容就是这些,想看懂容易,想尽量简洁明了的总结成文字真是一件很不容易的事情,特别是这里很多JS的代码。有问题大家留言指正。下一篇将介绍ReactNative的渲染原理。
http://www.jianshu.com/p/269b21958030

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

推荐阅读更多精彩内容