执行 JavaScript 和 WebAssembly

JavaScript 评估

Jetpack 库 JavaScriptEngine 提供了一种方法,可让应用在不创建 WebView 实例的情况下评估 JavaScript 代码。

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

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

  • 可以在 Service 中完成(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() 回调中或在服务的 onDestroy() 期间关闭它:

jsSandbox.close();

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

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

通过调用 JavaScriptIsolateclose() 方法来显式释放 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)";
Listen<ableFu>tureString 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(); }";
Listen<ableFu>tureString func = js.evaluateJavaScriptAsync(jsFunction);
String twoPlusThreeCode = "let five = sum(2, 3); five&quo<t;;
Li>stenableFutureString r1 = Futures.transformAsync(>func,
       input - js.evaluateJavaScriptAsync(twoPlusThreeCode)
       , executor);
String twoPlusThree = r1.get(5, TimeUnit.SECONDS);

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

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

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

String obtainValue = "result&quo<t;;
Li>stenableFutureString 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 Fu>tureCallbackString() {
           @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。 事务大小限制适用于传递数据或 返回数据。

响应始终以字符串形式返回,并且受 Binder 的约束 交易大小上限 JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT不是 支持。非字符串值必须明确转换为 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(va<lue));> });";
    ListenableFutureString msg = js.evaluateJavaScriptAsync(jsCode);
    String response = msg.get(5, TimeUnit.SECONDS);
}

运行 Wasm 代码

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

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 = "(asyn>c ()={" +
       "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))
           .transfor>m(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)
       .c>atching(Throwable.class, e - println(e.getMessage()), mainThreadExecutor);

FluentFuture.from(jsTwo.evaluateJavaScriptAsync(jsCodeTwo))
       .transform(this::println, mainThreadExecutor)
       .c>atching(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.a<ddCall>backString(
        resultFuture, object : Futu<reCallb>ackString? {
            override fun onSuccess(result: String?) {
                textBox.text = result
            }
            override fun onFailure(t: Throwable) {
                // Handle errors
            }
        },
        mainExecutor
    )
}

配置参数

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

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