javascript-native調(diào)用實(shí)現(xiàn)

在現(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在modulemodule-methodcallback都使用了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)于NativeFlushQueueSyncNativeFlushQueueAsync到下面再解釋歪沃。

這里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)判斷胚迫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市唾那,隨后出現(xiàn)的幾起案子访锻,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件朗若,死亡現(xiàn)場(chǎng)離奇詭異恼五,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)哭懈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門灾馒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人遣总,你說(shuō)我怎么就攤上這事睬罗。” “怎么了旭斥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵容达,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我垂券,道長(zhǎng)花盐,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任菇爪,我火速辦了婚禮算芯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凳宙。我一直安慰自己熙揍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布氏涩。 她就那樣靜靜地躺著届囚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪是尖。 梳的紋絲不亂的頭發(fā)上意系,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音饺汹,去河邊找鬼蛔添。 笑死,一個(gè)胖子當(dāng)著我的面吹牛首繁,可吹牛的內(nèi)容都是我干的作郭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼弦疮,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼夹攒!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起胁塞,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤咏尝,失蹤者是張志新(化名)和其女友劉穎压语,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體编检,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胎食,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了允懂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厕怜。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蕾总,靈堂內(nèi)的尸體忽然破棺而出粥航,到底是詐尸還是另有隱情,我是刑警寧澤生百,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布递雀,位于F島的核電站,受9級(jí)特大地震影響蚀浆,放射性物質(zhì)發(fā)生泄漏缀程。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一市俊、第九天 我趴在偏房一處隱蔽的房頂上張望杨凑。 院中可真熱鬧,春花似錦秕衙、人聲如沸蠢甲。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至搞糕,卻和暖如春勇吊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窍仰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工汉规, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人驹吮。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓针史,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親碟狞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子啄枕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,190評(píng)論 25 707
  • ? React Native(以下簡(jiǎn)稱RN)的目標(biāo)是用基于react的JavaScript寫(xiě)代碼,在iOS/A...
    Iceguest閱讀 3,579評(píng)論 0 10
  • React Native 是最近非匙逦郑火的一個(gè)話題频祝,介紹如何利用 React Native 進(jìn)行開(kāi)發(fā)的文章和書(shū)籍多如...
    零度_不結(jié)冰閱讀 677評(píng)論 0 1
  • 引言 React Native以其獨(dú)到的特性泌参,吸引著互聯(lián)網(wǎng)公司紛紛為之投入或多或少的人力。在實(shí)際的開(kāi)發(fā)過(guò)程中常空,開(kāi)發(fā)...
    Jason景閱讀 10,790評(píng)論 4 25
  • 0403晨讀感悟沽一。 1. 讀完這則故事想說(shuō)說(shuō)堅(jiān)持這件事。 做任何事都是一個(gè)信心建立的過(guò)程漓糙。像手機(jī)電量格一樣铣缠。0到1...
    老馮kk閱讀 275評(píng)論 2 11