JavaScript 评估
Jetpack 库 JavaScriptEngine 为应用提供了一种 评估 JavaScript 代码,而无需创建 WebView 实例。
对于需要非交互式 JavaScript 评估的应用,使用 JavaScriptEngine 库具有以下优势:
资源消耗量更低,因为不需要分配 WebView 实例。
可以在 Service 中完成(WorkManager 任务)。
多个独立环境,开销低,使应用能够 同时运行多个 JavaScript 代码段。
能够使用 API 调用传递大量数据。
基本用法
首先,创建一个 JavaScriptSandbox
实例。这表示
进程外 JavaScript 引擎。
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
建议使沙盒的生命周期与 需要 JavaScript 评估的组件。
例如,托管沙盒的组件可能是 Activity
或
Service
。可以使用单个 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
)。
该库还支持评估采用
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 代码导致此沙盒化进程 例如内存不足,导致应用的主要 也不会受到影响
沙盒崩溃会导致该沙盒中的所有隔离事件终止。最
一个明显的症状是,所有评估都开始以失败告终,
IsolateTerminatedException
。根据具体情况
特定例外情况,例如 SandboxDeadException
或
MemoryLimitExceededException
。
处理每次评估的崩溃并不总是切实可行。
此外,隔离可能会在用户明确请求之后终止,
评估。崩溃
处理逻辑可以进行集中化,方法是使用
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(...)
。
目前,参数允许指定最大堆大小和最大大小 评估返回值和错误。