在現(xiàn)在流行的多元框架中拣宏,最常見(jiàn)的就是JavaScript的應(yīng)用了。這里就來(lái)分析下react-native的實(shí)現(xiàn)澳腹。
react-native并不是只有一種實(shí)現(xiàn)。因?yàn)樗粌H僅支持JavaScriptCore來(lái)實(shí)現(xiàn)交互闻牡,也考慮到了某些場(chǎng)景下需要使用WebView來(lái)實(shí)現(xiàn),同時(shí)也有很多debug工具绳矩,需要將JavaScript的執(zhí)行環(huán)境轉(zhuǎn)移到瀏覽器罩润。大概的結(jié)構(gòu)如下:
------------------------------
| native |
------------------------------
|
bridge
ⅴ
|------------------------------|
| Executor |
|------------------------------|
| JSContext | WebView | Chrome |
|------------------------------|
其中執(zhí)行器部分(Executor)可隨意替換為不同實(shí)現(xiàn)。這里我們來(lái)分析下JSContext中的實(shí)現(xiàn)埋酬。
Module
要實(shí)現(xiàn)react-native這樣大型的框架,javascript就不能被散亂的放置烧栋,那么就必須進(jìn)行分模塊写妥。調(diào)用模塊時(shí)需要使用CommonJS或者ES6的方式。
var module = require('module')
import * as module from 'module'
同時(shí)也需要考慮到如此多的模塊审姓,一次性載入所帶來(lái)的性能損耗珍特,就必須采用惰性加載的方式。
隊(duì)列
和其他項(xiàng)目的實(shí)現(xiàn)方式類似魔吐,react-native依然使用了message queue來(lái)實(shí)現(xiàn)通信扎筒,而不是JavaScriptCore自帶的綁定功能,這是為了兼容上面說(shuō)的多Executor酬姆。
與其他方案不太相同的是嗜桌,react-native在module
,module-method
和callback
都使用了id: number
來(lái)取代名字辞色,個(gè)人猜測(cè)可能是為了性能考慮骨宠。
那么我們就JSContext這種情況來(lái)說(shuō)下整個(gè)通信實(shí)現(xiàn)的過(guò)程。
實(shí)現(xiàn)
這里使用console
來(lái)作為例子相满,這里使用JavaScriptCore的c接口是為了和react-native保持一致层亿,同時(shí)忽略了內(nèi)存問(wèn)題。
模塊表
觀察發(fā)送給JSContext的數(shù)據(jù)發(fā)現(xiàn)會(huì)有很多類似這樣的JSON數(shù)據(jù):
[
"WebSocketModule",
null,
["connect","send","sendBinary","ping","close","addListener","removeListeners"]
]
可以看出來(lái)立美,[0]表示的是module名字匿又,而[2]表示的是module的方法,正式這一份表建蹄,才對(duì)應(yīng)了javascript和native雙方的indexId碌更,所有的通信都是對(duì)應(yīng)于這一份表來(lái)進(jìn)行的。
所以雙方都會(huì)有一份自己維護(hù)的模塊洞慎,而js的模塊表我們這里定義為
// id => module 這是native調(diào)用js module時(shí)针贬,傳遞的是id
var nativeModuleByIds = {}
// name => module 這是js調(diào)用js module時(shí),傳遞的是name
var nativeModules = {}
載入模塊
在javascript端拢蛋,如果需要載入模塊桦他,那么我們會(huì)使用
var console = require('console')
那么在JSContext還沒(méi)有console模塊的情況下如何進(jìn)行初始化呢?這里就需要一個(gè)NativeRequire
,來(lái)載入native模塊快压,結(jié)合上面的模塊配置表圆仔,require
的實(shí)現(xiàn)如下:
var NativeRequire
function require(moduleName) {
if (nativeModules[moduleName]) {
return nativeModules[moduleName]
}
return NativeRequire(moduleName)
}
NativeRequire
在初始化JSContext時(shí),我們就需要為通信做好連接的準(zhǔn)備蔫劣,直接注入3個(gè)方法坪郭。(這里react-native其實(shí)還有另外一個(gè)方式觸發(fā)require,通過(guò)nativeModuleProxy
對(duì)象的getProperty
來(lái)觸發(fā)脉幢,這里討論最原始的require
方式)
JSClassDefinition definition = kJSClassDefinitionEmpty;
JSClassRef global = JSClassCreate(&definition);
g_ctx = JSGlobalContextCreate(global);
JSObjectRef globalObj = JSContextGetGlobalObject(g_ctx);
{
JSStringRef name = JSStringCreateWithCFString(CFSTR("NativeRequire"));
JSObjectRef obj = JSObjectMakeFunctionWithCallback(g_ctx, name, NativeRequire);
JSObjectSetProperty(g_ctx, globalObj, name, obj, kJSPropertyAttributeNone, nil);
}
{
JSStringRef name = JSStringCreateWithCFString(CFSTR("NativeFlushQueueSync"));
JSObjectRef obj = JSObjectMakeFunctionWithCallback(g_ctx, name, NativeFlushQueueSync);
JSObjectSetProperty(g_ctx, globalObj, name, obj, kJSPropertyAttributeNone, nil);
}
{
JSStringRef name = JSStringCreateWithCFString(CFSTR("NativeFlushQueueAsync"));
JSObjectRef obj = JSObjectMakeFunctionWithCallback(g_ctx, name, NativeFlushQueueAsync);
JSObjectSetProperty(g_ctx, globalObj, name, obj, kJSPropertyAttributeNone, nil);
}
關(guān)于NativeFlushQueueSync
和NativeFlushQueueAsync
到下面再解釋歪沃。
這里native的模塊表就不實(shí)現(xiàn)了,直接使用["console", null, ["log", "getName"], [1]]
嫌松。
JSValueRef NativeRequire (
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception) {
if (argumentCount == 1) {
JSValueRef jsModuleName = arguments[0];
if (JSValueIsString(g_ctx, jsModuleName)) {
char buffer[128] = {0};
JSStringGetUTF8CString(JSValueToStringCopy(g_ctx, jsModuleName, nil), buffer, 128);
// 0. 當(dāng)js調(diào)用"NativeRequire('console')"的時(shí)候
// 1. 我們會(huì)在本地的模塊表里根據(jù)名字去查找
// 這里就簡(jiǎn)單的strcmp來(lái)表示
if (strcmp(buffer, "console") == 0) {
CFStringRef config = CFSTR("[\"console\", null, [\"log\", \"getName\"], [1]]");
// 2. 構(gòu)造js對(duì)應(yīng)的模塊表沪曙,這里的順序必須和native是一一對(duì)應(yīng)的
// [ moduleName, constants, methods, async indexes ]
JSValueRef jsonConfig = JSValueMakeFromJSONString(g_ctx, JSStringCreateWithCFString(config));
JSObjectRef global = JSContextGetGlobalObject(g_ctx);
JSValueRef genNativeModules = JSObjectGetProperty(g_ctx, global, JSStringCreateWithCFString(CFSTR("genNativeModules")), nil);
JSValueRef args[] = {JSValueMakeNumber(g_ctx, ConsoleModuleId), jsonConfig};
// call JS => genNativeModules(moduleId, config)
// 3. 調(diào)用js,初始化native模塊萎羔,將函數(shù)表中的string轉(zhuǎn)換為function實(shí)現(xiàn)
// 這里接下節(jié)
JSValueRef module = JSObjectCallAsFunction(g_ctx, JSValueToObject(g_ctx, genNativeModules, nil), global, 2, args, nil);
return module;
}
}
}
return JSValueMakeNull(g_ctx);
}
這里會(huì)同步調(diào)用初始化模塊方法液走,并且將模塊返回給JSContext。
但是可以發(fā)現(xiàn)模塊表中的方法都是string贾陷,也就是方法名缘眶,我們?nèi)绾稳ナ褂?code>console.log()這樣的方法呢?這里就需要中間的初始化模塊這個(gè)作用了髓废。
初始化模塊
回到上節(jié)的第三步巷懈,此時(shí)native傳給js一個(gè)模塊表,讓js去構(gòu)造這個(gè)模塊慌洪。讓我們回到j(luò)s:
function genNativeModules(moduleId, config) {
let [name, constants, methods, asyncs] = config
let module = {}
// 這里將所有的方法名都轉(zhuǎn)換為function
methods.forEach(function(method, methodId) {
module[method] = function (args) {
// call native flush
}
}, this);
nativeModules[name] = module
nativeModuleByIds[moduleId] = module
return module
}
這樣便把string轉(zhuǎn)換為function了砸喻,可以像正常的js方法那樣使用了。
到這里注冊(cè)js模塊已經(jīng)完成蒋譬,下面來(lái)說(shuō)說(shuō)調(diào)用的過(guò)程割岛。
同步方法的調(diào)用
同步方法的調(diào)用對(duì)于JSContext來(lái)說(shuō)會(huì)簡(jiǎn)單很多,而對(duì)于很多基于webview的實(shí)現(xiàn)來(lái)說(shuō)就會(huì)麻煩一些犯助,因?yàn)閰?shù)不能直接編碼在url中癣漆,最后我們來(lái)討論下這個(gè)問(wèn)題。
上節(jié)說(shuō)到將方法名轉(zhuǎn)換為function剂买,那么function具體實(shí)現(xiàn)是怎么樣的呢惠爽?
首先來(lái)看看同步方法的實(shí)現(xiàn):
module[method] = function (args) {
return NativeFlushQueueSync(moduleId, methodId, ...args)
}
這里的NativeFlushQueueSync
方法就是一開(kāi)始我們注入的方法,作用是執(zhí)行對(duì)應(yīng)模塊的對(duì)應(yīng)方法瞬哼。
JSValueRef NativeFlushQueueSync (
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception) {
if (argumentCount == 3) {
// 這里通過(guò)查找native的模塊表婚肆,查找到對(duì)應(yīng)的方法,并執(zhí)行
if (JSValueIsNumber(g_ctx, arguments[0]) && JSValueIsNumber(g_ctx, arguments[1])) {
if (JSValueToNumber(g_ctx, arguments[0], nil) == ConsoleModuleId) {
if (JSValueToNumber(g_ctx, arguments[1], nil) == 0) {
// call Native <= console.log
if (JSValueIsString(g_ctx, arguments[2])) {
// console.log轉(zhuǎn)換為NSLog
NSString *str = (__bridge NSString *)JSStringCopyCFString(NULL, JSValueToStringCopy(g_ctx, arguments[2], nil));
NSLog(@"%@", str);
}
}
}
}
}
return JSValueMakeNull(g_ctx);
}
然而react-native并沒(méi)有完全嚴(yán)格上的同步執(zhí)行方法坐慰。因?yàn)楹芏嗾{(diào)用UI層的功能必須在主線程上较性,而JSContext是在自己的線程中執(zhí)行,所以如果需要嚴(yán)格的同步執(zhí)行,需要阻塞JS線程赞咙。而幾乎所有功能都是不需要執(zhí)行結(jié)果的(return void)责循,所以只要觸發(fā)native去執(zhí)行該方法就行了,無(wú)需等待執(zhí)行完再返回攀操。而需要有返回值的接口都被設(shè)計(jì)成異步的了院仿。
異步回調(diào)
說(shuō)到異步回調(diào),大家用的方案好像都是一樣的速和,那就是callbackId
歹垫。
var messageQueue = {}
var messageQueueId = 0
function JsMessageQueueAdd(args) {
messageQueueId ++
messageQueue[messageQueueId] = args
return messageQueueId
}
function JsMessageQueueFlush(queueId, args) {
let callback = messageQueue[queueId]
if (callback && typeof(callback) === 'function') {
callback(args)
}
}
創(chuàng)建異步module方法的方式會(huì)有點(diǎn)不一樣:
module[method] = function (args) {
let queueId = JsMessageQueueAdd(args)
NativeFlushQueueAsync(moduleId, methodId, queueId)
}
然后來(lái)看看native的實(shí)現(xiàn):
JSValueRef NativeFlushQueueAsync (
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception) {
if (argumentCount == 3) {
if (JSValueIsNumber(g_ctx, arguments[0]) && JSValueIsNumber(g_ctx, arguments[1])) {
if (JSValueToNumber(g_ctx, arguments[0], nil) == ConsoleModuleId) {
if (JSValueToNumber(g_ctx, arguments[1], nil) == 1) {
// call Native <= console.getName
JSValueRef queueId = arguments[2];
NSInteger queueIdCopy = JSValueToNumber(g_ctx, queueId, nil);
dispatch_async(dispatch_get_main_queue(), ^{
JSObjectRef global = JSContextGetGlobalObject(g_ctx);
JSValueRef flush = JSObjectGetProperty(g_ctx, global, JSStringCreateWithCFString(CFSTR("JsMessageQueueFlush")), nil);
JSValueRef args[] = {
JSValueMakeNumber(g_ctx, queueIdCopy), // callback queueId
JSValueMakeString(g_ctx, JSStringCreateWithCFString(CFSTR("My iPhone")))
};
// call JS => JsMessageQueueFlush(queueId, args)
JSObjectCallAsFunction(g_ctx, JSValueToObject(g_ctx, flush, nil), nil, 2, args, nil);
});
}
}
}
}
return JSValueMakeNull(g_ctx);
}
可以看到和同步方式的區(qū)別是就是回調(diào)會(huì)緩存在隊(duì)列里。
應(yīng)用
var console = require('console')
console.log('Hello Javascript!')
console.getName(function (name) {
console.log(`Hello ${name}`)
})
// output:
Hello Javascript!
Hello My iPhone
裝飾
實(shí)際情況不會(huì)這么簡(jiǎn)單颠放,js也不會(huì)直接使用native提供的模塊的排惨,一般會(huì)包裝一層。比如像這樣
var nativeLog = NativeRequire('NSLog')
var console = {
log: (args) => NSLog(args),
info: (args) => NSLog('[INFO]', ...args),
error: (args) => NSLog('[ERROR]', ...args)
}
export default console
實(shí)際
真實(shí)情況不會(huì)像上面那么簡(jiǎn)單慈迈,需要考慮到多線程若贮,每個(gè)module的運(yùn)行線程省有,js消息隊(duì)列等保證js的安全順序執(zhí)行痒留。
WebView
其他項(xiàng)目的方案也是類似的,但也有少許的不同蠢沿。
比如NativeRequire伸头,在Web里面除了通過(guò)iframe來(lái)實(shí)現(xiàn),還可以通過(guò)script
標(biāo)簽來(lái)導(dǎo)入模塊文件舷蟀。
var script = document.createElement('script')
script.setAttribute('src', 'file://module.js')
document.head.appendChild(script)
同時(shí)由于web通過(guò)url傳遞參數(shù)的限制恤磷,所以web的參數(shù)傳遞是通過(guò)native去主動(dòng)拉取的。大概的流程如下:
[web] call native --> push <call info> --(iframe url)-->
[native] get <call info> --(executeJs)-->
[web] pop <call info> -->
[native] call ***
同時(shí)很多方案野宜,會(huì)使用名字來(lái)傳遞模塊和方法扫步,這樣做最簡(jiǎn)單也最直接。但是如果存在頻繁交互的過(guò)程可能會(huì)降低性能匈子。
最后
總的來(lái)說(shuō)河胎,javascript-native交互還是挺簡(jiǎn)單的,只要在初始的設(shè)計(jì)上比較符合現(xiàn)在與未來(lái)的發(fā)展虎敦,還是可以做到很靈活的游岳。至于使用哪種方案,做到什么樣的程度其徙,可以依據(jù)自身的需求來(lái)判斷胚迫。