javascript-native调用实现

在现在流行的多元框架中,最常见的就是JavaScript的应用了。这里就来分析下react-native的实现。

react-native并不是只有一种实现。因为他不仅仅支持JavaScriptCore来实现交互,也考虑到了某些场景下需要使用WebView来实现,同时也有很多debug工具,需要将JavaScript的执行环境转移到浏览器。大概的结构如下:

 ------------------------------
|            native            |
 ------------------------------
               |
             bridge
               ⅴ
|------------------------------|
|            Executor          |
|------------------------------|
| JSContext | WebView | Chrome |
|------------------------------|

其中执行器部分(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保持一致,同时忽略了内存问题。

模块表

观察发送给JSContext的数据发现会有很多类似这样的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其实还有另外一个方式触发require,通过nativeModuleProxy对象的getProperty来触发,这里讨论最原始的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实现
                // 这里接下节
                JSValueRef module = JSObjectCallAsFunction(g_ctx, JSValueToObject(g_ctx, genNativeModules, nil), global, 2, args, nil);
                return module;
            }
        }
    }

    return JSValueMakeNull(g_ctx);
}

这里会同步调用初始化模块方法,并且将模块返回给JSContext。

但是可以发现模块表中的方法都是string,也就是方法名,我们如何去使用console.log()这样的方法呢?这里就需要中间的初始化模块这个作用了。

初始化模块

回到上节的第三步,此时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中,最后我们来讨论下这个问题。

上节说到将方法名转换为function,那么function具体实现是怎么样的呢?

首先来看看同步方法的实现:

module[method] = function (args) {
    return NativeFlushQueueSync(moduleId, methodId, ...args)
}

这里的NativeFlushQueueSync方法就是一开始我们注入的方法,作用是执行对应模块的对应方法。

JSValueRef NativeFlushQueueSync (
  JSContextRef ctx,
  JSObjectRef function,
  JSObjectRef thisObject,
  size_t argumentCount,
  const JSValueRef arguments[],
  JSValueRef *exception) {

    if (argumentCount == 3) {
        // 这里通过查找native的模块表,查找到对应的方法,并执行
        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并没有完全严格上的同步执行方法。因为很多调用UI层的功能必须在主线程上,而JSContext是在自己的线程中执行,所以如果需要严格的同步执行,需要阻塞JS线程。而几乎所有功能都是不需要执行结果的(return void),所以只要触发native去执行该方法就行了,无需等待执行完再返回。而需要有返回值的接口都被设计成异步的了。

异步回调

说到异步回调,大家用的方案好像都是一样的,那就是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)
    }
}

创建异步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);
}

可以看到和同步方式的区别是就是回调会缓存在队列里。

应用
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的安全顺序执行。

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交互还是挺简单的,只要在初始的设计上比较符合现在与未来的发展,还是可以做到很灵活的。至于使用哪种方案,做到什么样的程度,可以依据自身的需求来判断。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容