JavaScript 执行
Jetpack 库 JavaScriptEngine 提供了一种在不创建 WebView 实例的情况下执行 JavaScript 代码的方式。
对于需要非交互式 JavaScript 执行的应用,使用 JavaScriptEngine 库具有以下优势:
资源消耗更低,因为无需分配 WebView 实例。
可以在 Service(WorkManager 任务)中完成。
多个隔离的环境,开销较低,使应用能够同时运行多个 JavaScript 片段。
可以通过 API 调用传递大量数据。
基本用法
首先,创建一个 JavaScriptSandbox
实例。它表示与进程外 JavaScript 引擎的连接。
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
建议将沙盒的生命周期与需要 JavaScript 执行的组件的生命周期对齐。
例如,托管沙盒的组件可以是 Activity
或 Service
。单个 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
)的 isolate 实例会导致 IsolateTerminatedException
。如果实现支持 JS_FEATURE_ISOLATE_TERMINATION
,如页面后面的处理沙盒崩溃部分所述,isolate 会在后台随后清理。否则,清理会推迟到所有待处理的执行完成或沙盒关闭为止。
应用可以从任何线程创建和访问 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
关键字。如果沙盒支持某些特性,可能支持其他返回类型(例如,解析为 String
的 Promise
)。
该库还支持以 AssetFileDescriptor
或 ParcelFileDescriptor
形式执行脚本。有关详细信息,请参阅 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 代码导致此沙盒进程崩溃(例如,由于内存耗尽),应用的主进程将不受影响。
沙盒崩溃会导致该沙盒中的所有 isolate 终止。最明显的症状是所有执行都将开始失败并抛出 IsolateTerminatedException
。根据具体情况,可能会抛出更具体的异常,例如 SandboxDeadException
或 MemoryLimitExceededException
。
为每次单独的执行处理崩溃并不总是可行。此外,由于后台任务或在其他 isolate 中执行,isolate 可能会在未明确请求执行的情况下终止。可以通过使用 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。通用事务大小限制适用于每个传递数据或返回数据的调用。
响应始终以 String 形式返回,如果不支持 JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
,则受 Binder 事务最大大小限制。非字符串值必须显式转换为 JavaScript String,否则返回空字符串。如果支持 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
实例相互独立,互不共享任何东西。以下代码片段的结果是
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 支持
要在 Kotlin 协程中使用此 Jetpack 库,请添加对 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(...)
。
当前参数允许指定最大堆大小以及执行返回值和错误的最大大小。