在現在流行的多元框架中,最常見的就是JavaScript的應用了。這里就來分析下react-native的實現。
react-native并不是只有一種實現。因為他不僅僅支持JavaScriptCore來實現交互,也考慮到了某些場景下需要使用WebView來實現,同時也有很多debug工具,需要將JavaScript的執(zhí)行環(huán)境轉移到瀏覽器。大概的結構如下:
------------------------------
| native |
------------------------------
|
bridge
ⅴ
|------------------------------|
| Executor |
|------------------------------|
| JSContext | WebView | Chrome |
|------------------------------|
其中執(zhí)行器部分(Executor)可隨意替換為不同實現。這里我們來分析下JSContext中的實現。
Module
要實現react-native這樣大型的框架,javascript就不能被散亂的放置,那么就必須進行分模塊。調用模塊時需要使用CommonJS或者ES6的方式。
var module = require('module')
import * as module from 'module'
同時也需要考慮到如此多的模塊,一次性載入所帶來的性能損耗,就必須采用惰性加載的方式。
隊列
和其他項目的實現方式類似,react-native依然使用了message queue來實現通信,而不是JavaScriptCore自帶的綁定功能,這是為了兼容上面說的多Executor。
與其他方案不太相同的是,react-native在module,module-method和callback都使用了id: number來取代名字,個人猜測可能是為了性能考慮。
那么我們就JSContext這種情況來說下整個通信實現的過程。
實現
這里使用console來作為例子,這里使用JavaScriptCore的c接口是為了和react-native保持一致,同時忽略了內存問題。
模塊表
觀察發(fā)送給JSContext的數據發(fā)現會有很多類似這樣的JSON數據:
[
"WebSocketModule",
null,
["connect","send","sendBinary","ping","close","addListener","removeListeners"]
]
可以看出來,[0]表示的是module名字,而[2]表示的是module的方法,正式這一份表,才對應了javascript和native雙方的indexId,所有的通信都是對應于這一份表來進行的。
所以雙方都會有一份自己維護的模塊,而js的模塊表我們這里定義為
// id => module 這是native調用js module時,傳遞的是id
var nativeModuleByIds = {}
// name => module 這是js調用js module時,傳遞的是name
var nativeModules = {}
載入模塊
在javascript端,如果需要載入模塊,那么我們會使用
var console = require('console')
那么在JSContext還沒有console模塊的情況下如何進行初始化呢?這里就需要一個NativeRequire,來載入native模塊,結合上面的模塊配置表,require的實現如下:
var NativeRequire
function require(moduleName) {
if (nativeModules[moduleName]) {
return nativeModules[moduleName]
}
return NativeRequire(moduleName)
}
NativeRequire
在初始化JSContext時,我們就需要為通信做好連接的準備,直接注入3個方法。(這里react-native其實還有另外一個方式觸發(fā)require,通過nativeModuleProxy對象的getProperty來觸發(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);
}
關于NativeFlushQueueSync和NativeFlushQueueAsync到下面再解釋。
這里native的模塊表就不實現了,直接使用["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. 當js調用"NativeRequire('console')"的時候
// 1. 我們會在本地的模塊表里根據名字去查找
// 這里就簡單的strcmp來表示
if (strcmp(buffer, "console") == 0) {
CFStringRef config = CFSTR("[\"console\", null, [\"log\", \"getName\"], [1]]");
// 2. 構造js對應的模塊表,這里的順序必須和native是一一對應的
// [ 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. 調用js,初始化native模塊,將函數表中的string轉換為function實現
// 這里接下節(jié)
JSValueRef module = JSObjectCallAsFunction(g_ctx, JSValueToObject(g_ctx, genNativeModules, nil), global, 2, args, nil);
return module;
}
}
}
return JSValueMakeNull(g_ctx);
}
這里會同步調用初始化模塊方法,并且將模塊返回給JSContext。
但是可以發(fā)現模塊表中的方法都是string,也就是方法名,我們如何去使用console.log()這樣的方法呢?這里就需要中間的初始化模塊這個作用了。
初始化模塊
回到上節(jié)的第三步,此時native傳給js一個模塊表,讓js去構造這個模塊。讓我們回到js:
function genNativeModules(moduleId, config) {
let [name, constants, methods, asyncs] = config
let module = {}
// 這里將所有的方法名都轉換為function
methods.forEach(function(method, methodId) {
module[method] = function (args) {
// call native flush
}
}, this);
nativeModules[name] = module
nativeModuleByIds[moduleId] = module
return module
}
這樣便把string轉換為function了,可以像正常的js方法那樣使用了。
到這里注冊js模塊已經完成,下面來說說調用的過程。
同步方法的調用
同步方法的調用對于JSContext來說會簡單很多,而對于很多基于webview的實現來說就會麻煩一些,因為參數不能直接編碼在url中,最后我們來討論下這個問題。
上節(jié)說到將方法名轉換為function,那么function具體實現是怎么樣的呢?
首先來看看同步方法的實現:
module[method] = function (args) {
return NativeFlushQueueSync(moduleId, methodId, ...args)
}
這里的NativeFlushQueueSync方法就是一開始我們注入的方法,作用是執(zhí)行對應模塊的對應方法。
JSValueRef NativeFlushQueueSync (
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception) {
if (argumentCount == 3) {
// 這里通過查找native的模塊表,查找到對應的方法,并執(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轉換為NSLog
NSString *str = (__bridge NSString *)JSStringCopyCFString(NULL, JSValueToStringCopy(g_ctx, arguments[2], nil));
NSLog(@"%@", str);
}
}
}
}
}
return JSValueMakeNull(g_ctx);
}
然而react-native并沒有完全嚴格上的同步執(zhí)行方法。因為很多調用UI層的功能必須在主線程上,而JSContext是在自己的線程中執(zhí)行,所以如果需要嚴格的同步執(zhí)行,需要阻塞JS線程。而幾乎所有功能都是不需要執(zhí)行結果的(return void),所以只要觸發(fā)native去執(zhí)行該方法就行了,無需等待執(zhí)行完再返回。而需要有返回值的接口都被設計成異步的了。
異步回調
說到異步回調,大家用的方案好像都是一樣的,那就是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方法的方式會有點不一樣:
module[method] = function (args) {
let queueId = JsMessageQueueAdd(args)
NativeFlushQueueAsync(moduleId, methodId, queueId)
}
然后來看看native的實現:
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ū)別是就是回調會緩存在隊列里。
應用
var console = require('console')
console.log('Hello Javascript!')
console.getName(function (name) {
console.log(`Hello ${name}`)
})
// output:
Hello Javascript!
Hello My iPhone
裝飾
實際情況不會這么簡單,js也不會直接使用native提供的模塊的,一般會包裝一層。比如像這樣
var nativeLog = NativeRequire('NSLog')
var console = {
log: (args) => NSLog(args),
info: (args) => NSLog('[INFO]', ...args),
error: (args) => NSLog('[ERROR]', ...args)
}
export default console
實際
真實情況不會像上面那么簡單,需要考慮到多線程,每個module的運行線程,js消息隊列等保證js的安全順序執(zhí)行。
WebView
其他項目的方案也是類似的,但也有少許的不同。
比如NativeRequire,在Web里面除了通過iframe來實現,還可以通過script標簽來導入模塊文件。
var script = document.createElement('script')
script.setAttribute('src', 'file://module.js')
document.head.appendChild(script)
同時由于web通過url傳遞參數的限制,所以web的參數傳遞是通過native去主動拉取的。大概的流程如下:
[web] call native --> push <call info> --(iframe url)-->
[native] get <call info> --(executeJs)-->
[web] pop <call info> -->
[native] call ***
同時很多方案,會使用名字來傳遞模塊和方法,這樣做最簡單也最直接。但是如果存在頻繁交互的過程可能會降低性能。
最后
總的來說,javascript-native交互還是挺簡單的,只要在初始的設計上比較符合現在與未來的發(fā)展,還是可以做到很靈活的。至于使用哪種方案,做到什么樣的程度,可以依據自身的需求來判斷。