javascript-native調用實現

在現在流行的多元框架中,最常見的就是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在modulemodule-methodcallback都使用了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);
}

關于NativeFlushQueueSyncNativeFlushQueueAsync到下面再解釋。

這里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ā)展,還是可以做到很靈活的。至于使用哪種方案,做到什么樣的程度,可以依據自身的需求來判斷。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容