执行 JavaScript 和 WebAssembly

JavaScript 评估

Jetpack 库 JavaScriptEngine 提供了一种方法,使应用程序可以评估 JavaScript 代码,而无需创建 WebView 实例。

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

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

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

  • 多个隔离环境,开销低,使应用程序能够同时运行多个 JavaScript 代码片段。

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

基本用法

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

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

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

例如,托管沙盒的组件可以是 ActivityService。单个 Service 可用于封装所有应用程序组件的 JavaScript 评估。

维护 JavaScriptSandbox 实例,因为它的分配相当昂贵。每个应用程序只允许一个 JavaScriptSandbox 实例。当应用程序尝试分配第二个 JavaScriptSandbox 实例时,将抛出 IllegalStateException。但是,如果需要多个执行环境,则可以分配多个 JavaScriptIsolate 实例。

当不再使用时,关闭沙盒实例以释放资源。 JavaScriptSandbox 实例实现 AutoCloseable 接口,允许针对简单的阻塞用例使用 try-with-resources。或者,确保 JavaScriptSandbox 实例生命周期由托管组件管理,在 Activity 的 onStop() 回调中或在 Service 的 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 类型;否则,库 API 会返回空值。JavaScript 代码不应该使用 return 关键字。如果沙盒支持某些功能,则可能支持其他返回类型(例如,解析为 StringPromise)。

该库还支持评估 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 事务限制。如果该功能不受支持,则所有数据都将通过 Binder 事务传递到 JavaScriptEngine。 常规事务大小限制 适用于传递数据或返回数据的每个调用。

响应始终 作为字符串返回,如果未支持 JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT,则受 Binder 事务最大大小限制。非字符串值必须显式转换为 JavaScript 字符串,否则将返回空字符串。如果支持 JS_FEATURE_PROMISE_RETURN 功能,JavaScript 代码可以返回一个解析为 String 的 Promise。

要将大型字节数组传递到 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 代码

WebAssembly (Wasm) 代码可以使用 provideNamedData(...) 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 实例彼此独立,互不共享任何内容。以下代码段会导致

来自 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(...)

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