javascript-native调用实现

2017-08-20  本文已影响167人  码农苍耳

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

上一篇下一篇

猜你喜欢

热点阅读