执行 JavaScript 和 WebAssembly

JavaScript 评估

Jetpack 库 JavaScriptEngine 为应用提供了一种 评估 JavaScript 代码,而无需创建 WebView 实例。

对于需要非交互式 JavaScript 评估的应用,使用 JavaScriptEngine 库具有以下优势:

  • 资源消耗量更低,因为不需要分配 WebView 实例。

  • 可以在 Service 中完成(WorkManager 任务)。

  • 多个独立环境,开销低,使应用能够 同时运行多个 JavaScript 代码段。

  • 能够使用 API 调用传递大量数据。

基本用法

首先,创建一个 JavaScriptSandbox 实例。这表示 进程外 JavaScript 引擎。

ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
               JavaScriptSandbox.createConnectedInstanceAsync(context);

建议使沙盒的生命周期与 需要 JavaScript 评估的组件。

例如,托管沙盒的组件可能是 ActivityService。可以使用单个 Service 来封装 JavaScript 评估 所有组件。

维护 JavaScriptSandbox 实例,因为它的分配合理 价格高昂。每个应用只能有一个 JavaScriptSandbox 实例。一个 IllegalStateException 在应用尝试分配 第二个 JavaScriptSandbox 实例。但是,如果有多个执行环境 可以分配多个 JavaScriptIsolate 实例。

当沙盒不再使用时,请关闭沙盒实例以释放资源。通过 JavaScriptSandbox 实例会实现 AutoCloseable 接口,该接口 允许在简单的阻塞用例中使用 try-with-resources。 或者,确保 JavaScriptSandbox 实例生命周期由 托管组件,在 activity 的 onStop() 回调中将其关闭,或者 在onDestroy()期间调用某项服务:

jsSandbox.close();

JavaScriptIsolate 实例表示要执行的上下文 JavaScript 代码。它们可在必要时进行分配,安全性较低 为不同来源的脚本设置边界或启用并发 JavaScript 因为 JavaScript 本质上是单线程的对 同一个实例具有相同的状态,因此您可以创建一些数据 先行处理,稍后在 JavaScriptIsolate 的同一实例中进行处理。

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

通过调用 close() 方法显式释放 JavaScriptIsolate。 关闭运行 JavaScript 代码的隔离实例 (具有不完整的 Future)会导致 IsolateTerminatedException。通过 之后会在后台清理隔离的 支持 JS_FEATURE_ISOLATE_TERMINATION,如 处理沙盒崩溃部分 页面。否则,将推迟清理,直到所有待处理的评估完成 或沙盒关闭。

应用可以通过以下方式创建和访问 JavaScriptIsolate 实例: 任何线程。

现在,应用已准备好执行一些 JavaScript 代码:

final String code = "function sum(a, b) { let r = a + b; return r.toString(); }; sum(3, 4)";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);

同一 JavaScript 代码段格式设置得当:

function sum(a, b) {
    let r = a + b;
    return r.toString(); // make sure we return String instance
};

// Calculate and evaluate the expression
// NOTE: We are not in a function scope and the `return` keyword
// should not be used. The result of the evaluation is the value
// the last expression evaluates to.
sum(3, 4);

代码段以 String 的形式传递,结果以 String 的形式传递。 请注意,调用 evaluateJavaScriptAsync() 将返回已评估的 JavaScript 代码中最后一个表达式的结果。此值必须为 JavaScript String 类型;否则,Library API 会返回空值。 JavaScript 代码不应使用 return 关键字。如果测试区 支持某些功能、其他返回值类型(例如 Promise 解析为 String)。

该库还支持评估采用 AssetFileDescriptorParcelFileDescriptor。请参阅 evaluateJavaScriptAsync(AssetFileDescriptor)evaluateJavaScriptAsync(ParcelFileDescriptor),了解更多详情。 这些 API 更适合根据磁盘上或应用中的文件进行评估 目录。

该库还支持控制台日志记录,可用于调试 目的。可使用 setConsoleCallback() 进行设置。

由于上下文仍然存在,因此您可以上传代码并执行多次 在 JavaScriptIsolate 的生命周期内:

String jsFunction = "function sum(a, b) { let r = a + b; return r.toString(); }";
ListenableFuture<String> func = js.evaluateJavaScriptAsync(jsFunction);
String twoPlusThreeCode = "let five = sum(2, 3); five";
ListenableFuture<String> r1 = Futures.transformAsync(func,
       input -> js.evaluateJavaScriptAsync(twoPlusThreeCode)
       , executor);
String twoPlusThree = r1.get(5, TimeUnit.SECONDS);

String fourPlusFiveCode = "sum(4, parseInt(five))";
ListenableFuture<String> r2 = Futures.transformAsync(func,
       input -> js.evaluateJavaScriptAsync(fourPlusFiveCode)
       , executor);
String fourPlusFive = r2.get(5, TimeUnit.SECONDS);

当然,变量也是永久性的,因此您可以继续 包含以下内容的代码段:

String defineResult = "let result = sum(11, 22);";
ListenableFuture<String> r3 = Futures.transformAsync(func,
       input -> js.evaluateJavaScriptAsync(defineResult)
       , executor);
String unused = r3.get(5, TimeUnit.SECONDS);

String obtainValue = "result";
ListenableFuture<String> r4 = Futures.transformAsync(func,
       input -> js.evaluateJavaScriptAsync(obtainValue)
       , executor);
String value = r4.get(5, TimeUnit.SECONDS);

例如,用于分配所有必要对象和 执行 JavaScript 代码的方式可能如下所示:

final ListenableFuture<JavaScriptSandbox> sandbox
       = JavaScriptSandbox.createConnectedInstanceAsync(this);
final ListenableFuture<JavaScriptIsolate> isolate
       = Futures.transform(sandbox,
               input -> (jsSandBox = input).createIsolate(),
               executor);
final ListenableFuture<String> js
       = Futures.transformAsync(isolate,
               isolate -> (jsIsolate = isolate).evaluateJavaScriptAsync("'PASS OK'"),
               executor);
Futures.addCallback(js,
       new FutureCallback<String>() {
           @Override
           public void onSuccess(String result) {
               text.append(result);
           }
           @Override
           public void onFailure(Throwable t) {
               text.append(t.getMessage());
           }
       },
       mainThreadExecutor);

建议您使用 try-with-resources 来确保分配给您的 资源就会释放,不再使用。关闭沙盒结果 在所有 JavaScriptIsolate 实例失败的所有待处理评估中 并使用 SandboxDeadException。当 JavaScript 评估遇到 如果出现错误,系统会创建 JavaScriptException。引用其子类 了解更具体的例外情况。

处理沙盒崩溃

所有 JavaScript 都在一个单独的沙盒进程中执行,该进程独立于 应用的主要进程如果 JavaScript 代码导致此沙盒化进程 例如内存不足,导致应用的主要 也不会受到影响

沙盒崩溃会导致该沙盒中的所有隔离事件终止。最 一个明显的症状是,所有评估都开始以失败告终, IsolateTerminatedException。根据具体情况 特定例外情况,例如 SandboxDeadExceptionMemoryLimitExceededException

处理每次评估的崩溃并不总是切实可行。 此外,隔离可能会在用户明确请求之后终止, 评估。崩溃 处理逻辑可以进行集中化,方法是使用 JavaScriptIsolate.addOnTerminatedCallback()

final ListenableFuture<JavaScriptSandbox> sandboxFuture =
    JavaScriptSandbox.createConnectedInstanceAsync(this);
final ListenableFuture<JavaScriptIsolate> isolateFuture =
    Futures.transform(sandboxFuture, sandbox -> {
      final IsolateStartupParameters startupParams = new IsolateStartupParameters();
      if (sandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE)) {
        startupParams.setMaxHeapSizeBytes(100_000_000);
      }
      return sandbox.createIsolate(startupParams);
    }, executor);
Futures.transform(isolateFuture,
    isolate -> {
      // Add a crash handler
      isolate.addOnTerminatedCallback(executor, terminationInfo -> {
        Log.e(TAG, "The isolate crashed: " + terminationInfo);
      });
      // Cause a crash (eventually)
      isolate.evaluateJavaScriptAsync("Array(1_000_000_000).fill(1)");
      return null;
    }, executor);

可选的沙盒功能

根据底层 WebView 版本,沙盒实现 提供了一组不同的功能因此,有必要查询每个所需的 功能(使用 JavaScriptSandbox.isFeatureSupported(...))。请务必注意 在调用依赖这些功能的方法之前,检查功能状态。

JavaScriptIsolate 方法可能并非在所有地方都可用 带有 RequiresFeature 注解,以便您更轻松地发现这些 调用。

传递参数

如果JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT为 则发送到 JavaScript 引擎的评估请求不会绑定 具体取决于 binder 事务限制。如果该功能不受支持, JavaScriptEngine 通过 Binder 事务发生。 事务大小限制适用于传递数据或 返回数据。

响应始终以字符串形式返回,并且受 Binder 的约束 交易大小上限 JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT不是 支持。非字符串值必须明确转换为 JavaScript 字符串 否则,系统会返回空字符串。如果状态为 JS_FEATURE_PROMISE_RETURN JavaScript 代码也可以返回一个 Promise 解析为 String

如需将大型字节数组传递给 JavaScriptIsolate 实例,您需要 可以使用 provideNamedData(...) API。使用此 API 不受 Binder 事务限制每个字节数组都必须使用唯一的 标识符,不可重复使用。

if (sandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER)) {
    js.provideNamedData("data-1", "Hello Android!".getBytes(StandardCharsets.US_ASCII));
    final String jsCode = "android.consumeNamedDataAsArrayBuffer('data-1').then((value) => { return String.fromCharCode.apply(null, new Uint8Array(value)); });";
    ListenableFuture<String> msg = js.evaluateJavaScriptAsync(jsCode);
    String response = msg.get(5, TimeUnit.SECONDS);
}

运行 Wasm 代码

可以使用 provideNamedData(...) 传递 WebAssembly (Wasm) 代码 API,然后以常规方式进行编译和执行,如下所示。

final byte[] hello_world_wasm = {
   0x00 ,0x61 ,0x73 ,0x6d ,0x01 ,0x00 ,0x00 ,0x00 ,0x01 ,0x0a ,0x02 ,0x60 ,0x02 ,0x7f ,0x7f ,0x01,
   0x7f ,0x60 ,0x00 ,0x00 ,0x03 ,0x03 ,0x02 ,0x00 ,0x01 ,0x04 ,0x04 ,0x01 ,0x70 ,0x00 ,0x01 ,0x05,
   0x03 ,0x01 ,0x00 ,0x00 ,0x06 ,0x06 ,0x01 ,0x7f ,0x00 ,0x41 ,0x08 ,0x0b ,0x07 ,0x18 ,0x03 ,0x06,
   0x6d ,0x65 ,0x6d ,0x6f ,0x72 ,0x79 ,0x02 ,0x00 ,0x05 ,0x74 ,0x61 ,0x62 ,0x6c ,0x65 ,0x01 ,0x00,
   0x03 ,0x61 ,0x64 ,0x64 ,0x00 ,0x00 ,0x09 ,0x07 ,0x01 ,0x00 ,0x41 ,0x00 ,0x0b ,0x01 ,0x01 ,0x0a,
   0x0c ,0x02 ,0x07 ,0x00 ,0x20 ,0x00 ,0x20 ,0x01 ,0x6a ,0x0b ,0x02 ,0x00 ,0x0b,
};
final String jsCode = "(async ()=>{" +
       "const wasm = await android.consumeNamedDataAsArrayBuffer('wasm-1');" +
       "const module = await WebAssembly.compile(wasm);" +
       "const instance = WebAssembly.instance(module);" +
       "return instance.exports.add(20, 22).toString();" +
       "})()";
// Ensure that the name has not been used before.
js.provideNamedData("wasm-1", hello_world_wasm);
FluentFuture.from(js.evaluateJavaScriptAsync(jsCode))
           .transform(this::println, mainThreadExecutor)
           .catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);
}

JavaScriptIsolate 分离

所有 JavaScriptIsolate 实例都是相互独立的, 分享任何事物。以下代码段会生成

Hi from AAA!5

Uncaught Reference Error: a is not defined

因为“jsTwo”实例无法查看在 “jsOne”。

JavaScriptIsolate jsOne = engine.obtainJavaScriptIsolate();
String jsCodeOne = "let x = 5; function a() { return 'Hi from AAA!'; } a() + x";
JavaScriptIsolate jsTwo = engine.obtainJavaScriptIsolate();
String jsCodeTwo = "a() + x";
FluentFuture.from(jsOne.evaluateJavaScriptAsync(jsCodeOne))
       .transform(this::println, mainThreadExecutor)
       .catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);

FluentFuture.from(jsTwo.evaluateJavaScriptAsync(jsCodeTwo))
       .transform(this::println, mainThreadExecutor)
       .catching(Throwable.class, e -> println(e.getMessage()), mainThreadExecutor);

Kotlin 支持

如需将此 Jetpack 库与 Kotlin 协程搭配使用,请向 kotlinx-coroutines-guava。这使得 ListenableFuture

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}

现在可以从协程作用域调用 Jetpack 库 API,如 如下所示:

// Launch a coroutine
lifecycleScope.launch {
    val jsSandbox = JavaScriptSandbox
            .createConnectedInstanceAsync(applicationContext)
            .await()
    val jsIsolate = jsSandbox.createIsolate()
    val resultFuture = jsIsolate.evaluateJavaScriptAsync("PASS")

    // Await the result
    textBox.text = resultFuture.await()
    // Or add a callback
    Futures.addCallback<String>(
        resultFuture, object : FutureCallback<String?> {
            override fun onSuccess(result: String?) {
                textBox.text = result
            }
            override fun onFailure(t: Throwable) {
                // Handle errors
            }
        },
        mainExecutor
    )
}

配置参数

请求隔离环境实例时,您可以调整其 配置。要调整配置,请将 IsolateStartupParameters 实例 JavaScriptSandbox.createIsolate(...)

目前,参数允许指定最大堆大小和最大大小 评估返回值和错误。