ReactNative iOS 互動

ZY_FlyWay發表於2017-07-28

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 =  @“callFunctionReturnFlushedQueueJSStringRef 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的整個路徑跑通了,我們梳理下整個流程看看。

[RCTBatchedBridge enqueueJSCall:@“AppRegistry.runApplication” args:["MGReactNative",{"rootTag":1,"initialProps":{}}]];
 RCTJavaScriptContext callFunctionOnModule:@"AppRegistr"
                      method:@"runApplication"
                   arguments:["MGReactNative",{"rootTag":1,"initialProps":{}}]
                    callback:(RCTJavaScriptCallback)onComplete
//main.js
 __fbBatchedBridge.callFunctionReturnFlushedQueue("AppRegistr","runApplication",["MGReactNative",{"rootTag":1,"initialProps":{}}])
//main.js
 BatchedBridge.__callFunction("AppRegistr","runApplication",["MGReactNative",{"rootTag":1,"initialProps":{}}])
//main.js
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
  2. 在React Native 的JS引擎初始化完成後,向JSContext注入ModuleConfig,賦值到JS全域性變數 __fbBatchedBridgeConfig
  3. 還記得那個N->JS大使---JS物件BatchedBridge嗎,BatchedBridge建立的時候會用__fbBatchedBridgeConfig變數裡Native模組名字列表定義一個同名的JS物件,但是是一個沒有任何方法的空物件,只增加了一個獲取方法陣列的get函式。此時初始化的操作已完成。
  4. 很久很久之後,有人用RCTAlertManager.alertWithArgs 呼叫了Native的程式碼,咳咳,這人是我,此時JS去獲取RCTAlertManager方法列表時,發現是空的,就呼叫Native提供的查詢函式nativeRequireModuleConfig 獲取RCTAlertManager物件的詳細的匯出資訊(方法列表),並定義成同名的JS函式,此函式轉調到OC的實現
  5. 此時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的渲染原理。



作者:肥皂V
連結:http://www.jianshu.com/p/269b21958030

相關文章