ReactNative iOS 互動
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>
);
},
)
Native 與 JS的互相呼叫
0.17版本的React Native JS引擎已經全部使用的是iOS自帶的JavaScriptCore,在JSContext提供的Native與js互相呼叫的基礎上,封裝出了自己的互調方法。下面是一張結構圖
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 來呼叫它。
更詳細的介紹可以學習這篇文章
JavaScriptCore
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的整個路徑跑通了,我們梳理下整個流程看看。
[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
對於詳細的如何匯出函式推薦閱讀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件事
- jsbundle檔案的下載或本地讀取(非同步)
- 初始化匯出給JS用的Native模組
- 初始化JS引擎
- 生成配置表,並注入到JS引擎中,
- 執行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的空物件
當呼叫了RCTAlertManager.alertWithArgs({message:'JS->Native Call',buttons:[{k1:'button1'}時,才會呼叫AlertManager 的get函式到Native裡查詢匯出的常量和函式,並定義到AlertManager中。
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的流程都走通了,
梳理一下整個流程。
總結一下
- Native初始化時, Native生成要匯出模組的名字列表(注意注意注意),僅僅是模組(類)名字列表, ModuleConfig
- 在React Native 的JS引擎初始化完成後,向JSContext注入ModuleConfig,賦值到JS全域性變數 __fbBatchedBridgeConfig
- 還記得那個N->JS大使---JS物件BatchedBridge嗎,BatchedBridge建立的時候會用__fbBatchedBridgeConfig變數裡Native模組名字列表定義一個同名的JS物件,但是是一個沒有任何方法的空物件,只增加了一個獲取方法陣列的get函式。此時初始化的操作已完成。
- 很久很久之後,有人用RCTAlertManager.alertWithArgs 呼叫了Native的程式碼,咳咳,這人是我,此時JS去獲取RCTAlertManager方法列表時,發現是空的,就呼叫Native提供的查詢函式nativeRequireModuleConfig 獲取RCTAlertManager物件的詳細的匯出資訊(方法列表),並定義成同名的JS函式,此函式轉調到OC的實現
- 此時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的改變或資料的變化。
這裡要注意的是,以上我們講到從 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
相關文章
- ReactNative與iOS的互動ReactiOS
- Android ReactNative資料互動AndroidReact
- RN互動iOSiOS
- ReactNative TabBarIOS和NavigatorIOSReacttabBariOS
- iOS開發-javaScript互動iOSJavaScript
- iOS與H5互動iOSH5
- ReactNative iOS原始碼解析(一)ReactiOS原始碼
- iOS中WKWebView互動使用總結iOSWebView
- iOS 與 JS 互動手冊 - JavaScriptCoreiOSJSJavaScript
- iOS 與JS Html常見互動iOSJSHTML
- ReactNative 踩坑之 iOS 原生元件ReactiOS元件
- ReactNative打離線包-ios篇ReactiOS
- [混編] iOS原生專案- iOS/flutter 程式碼互動iOSFlutter
- iOS高階-WebView & JavaScript互動(附DEMO)iOSWebViewJavaScript
- ReactNative專案打包(Android&&IOS)ReactAndroidiOS
- IOS小元件(7):小元件點選互動iOS元件
- iOS與JS互動之UIWebView-JavaScriptCore框架iOSJSUIWebViewJavaScript框架
- iOS Native混編Flutter互動實踐iOSFlutter
- 可以玩的UI-iOS互動式動畫UIiOS動畫
- ReactNative iOS真機除錯注意事項ReactiOS除錯
- IOS逆向--Tweak和app互動方案【程式通訊】iOSAPP
- iOS與JS互動之UIWebView協議攔截iOSJSUIWebView協議
- h5和ios互動要注意什麼?H5iOS
- H5與安卓/IOS進行原生互動H5安卓iOS
- iOS 自定義的卡片流互動控制元件iOS控制元件
- iOS 與 JS 互動開發知識總結iOSJS
- iOS開發:網頁JS與OC互動(JavaScriptCore)iOS網頁JSJavaScript
- ReactNative原始碼篇:啟動流程React原始碼
- iOS SceneKit顯示與互動3D建模(二)iOS3D
- iOS - SceneKit顯示與互動3D建模(一)iOS3D
- iOS中按鈕無法互動的5個原因iOS
- [MAUI]模仿iOS多工切換卡片滑動的互動實現UIiOS
- 社交場景下iOS訊息流互動層實踐iOS
- WKWebViewJavascriptBridge - 優雅的 iOS 與 JS 互動層框架(Swift)WebViewJavaScriptiOSJS框架Swift
- iosUIWebView與js的簡單互動swift3版iOSUIWebViewJSSwift
- iOS javascript與object-c的互動(TSY版本-就是本人)iOSJavaScriptObject
- ReactNative填坑之旅–與Native通訊之iOS篇ReactiOS
- ReactNative專案自動化打包釋出React