Đánh giá JavaScript
Thư viện Jetpack JavaScriptEngine cung cấp một cách để ứng dụng đánh giá mã JavaScript mà không cần tạo một phiên bản WebView.
Đối với các ứng dụng yêu cầu đánh giá JavaScript không tương tác, hãy sử dụng phương thức Thư viện JavaScriptEngine có các ưu điểm sau:
Giảm mức tiêu thụ tài nguyên do không cần phân bổ WebView thực thể.
Có thể thực hiện trong một Dịch vụ (tác vụ WorkManager).
Nhiều môi trường tách biệt với mức hao tổn thấp, cho phép ứng dụng chạy nhiều đoạn mã JavaScript cùng lúc.
Có thể truyền một lượng lớn dữ liệu bằng cách sử dụng lệnh gọi API.
Cách sử dụng cơ bản
Để bắt đầu, hãy tạo một thực thể của JavaScriptSandbox
. Điều này thể hiện
kết nối với công cụ JavaScript ngoài quy trình.
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
Bạn nên căn chỉnh vòng đời của hộp cát với vòng đời của thành phần cần đánh giá JavaScript.
Ví dụ: thành phần lưu trữ hộp cát có thể là Activity
hoặc
Service
. Bạn có thể dùng một Service
để đóng gói hoạt động đánh giá JavaScript
cho tất cả thành phần của ứng dụng.
Duy trì thực thể JavaScriptSandbox
vì quá trình phân bổ của thực thể này khá công bằng
rất đắt đỏ. Mỗi ứng dụng chỉ được phép có một phiên bản JavaScriptSandbox
. Một
Hệ thống sẽ gửi IllegalStateException
khi một ứng dụng cố gắng phân bổ phần tử tương ứng
thực thể thứ hai của JavaScriptSandbox
. Tuy nhiên, nếu nhiều môi trường thực thi
là bắt buộc, bạn có thể phân bổ nhiều thực thể JavaScriptIsolate
.
Khi không còn sử dụng hộp cát nữa, hãy đóng phiên bản hộp cát để giải phóng tài nguyên. Chiến lược phát hành đĩa đơn
Thực thể JavaScriptSandbox
triển khai giao diện AutoCloseable
. Giao diện này
cho phép sử dụng tính năng thử tài nguyên đối với các trường hợp sử dụng chặn đơn giản.
Ngoài ra, hãy đảm bảo vòng đời của thực thể JavaScriptSandbox
được quản lý bằng
thành phần lưu trữ, đóng thành phần đó trong lệnh gọi lại onStop()
cho một Hoạt động hoặc
trong onDestroy()
đối với một Dịch vụ:
jsSandbox.close();
Thực thể JavaScriptIsolate
đại diện cho ngữ cảnh để thực thi
Mã JavaScript. Chúng có thể được phân bổ khi cần, nhằm cung cấp khả năng bảo mật yếu
các ranh giới cho các tập lệnh có nguồn gốc khác nhau hoặc cho phép JavaScript đồng thời
vì JavaScript về bản chất là đơn luồng. Lệnh gọi tiếp theo tới
cùng một phiên bản có cùng trạng thái, do đó có thể tạo một số dữ liệu
trước rồi xử lý vào lúc khác trong cùng một phiên bản của JavaScriptIsolate
.
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
Giải phóng JavaScriptIsolate
một cách rõ ràng bằng cách gọi phương thức close()
.
Đóng một thực thể tách biệt đang chạy mã JavaScript
(có Future
không hoàn chỉnh) sẽ dẫn đến IsolateTerminatedException
. Chiến lược phát hành đĩa đơn
sau đó được dọn dẹp trong nền nếu quá trình triển khai
hỗ trợ JS_FEATURE_ISOLATE_TERMINATION
, như được mô tả trong
xử lý sự cố hộp cát sau này
. Nếu không, quá trình dọn dẹp sẽ bị trì hoãn cho đến khi tất cả lượt đánh giá đang chờ xử lý được xử lý xong
đã hoàn tất hoặc hộp cát đã đóng.
Ứng dụng có thể tạo và truy cập vào một thực thể JavaScriptIsolate
từ
bất kỳ chuỗi nào.
Bây giờ, ứng dụng đã sẵn sàng thực thi một số mã 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);
Cùng một đoạn mã JavaScript được định dạng độc đáo:
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);
Đoạn mã được truyền dưới dạng String
và kết quả được phân phối dưới dạng String
.
Lưu ý rằng việc gọi evaluateJavaScriptAsync()
sẽ trả về kết quả được đánh giá
kết quả của biểu thức cuối cùng trong mã JavaScript. Phải
của loại JavaScript String
; nếu không, API thư viện sẽ trả về giá trị trống.
Mã JavaScript không được sử dụng từ khóa return
. Nếu hộp cát
hỗ trợ một số tính năng, các kiểu dữ liệu trả về bổ sung (ví dụ: Promise
phân giải thành String
).
Thư viện này cũng hỗ trợ việc đánh giá các tập lệnh có dạng
AssetFileDescriptor
hoặc ParcelFileDescriptor
. Xem
evaluateJavaScriptAsync(AssetFileDescriptor)
và
evaluateJavaScriptAsync(ParcelFileDescriptor)
để biết thêm chi tiết.
Các API này phù hợp hơn cho việc đánh giá từ một tệp trên đĩa hoặc trong ứng dụng
.
Thư viện này cũng hỗ trợ tính năng ghi nhật ký bảng điều khiển, có thể dùng để gỡ lỗi
. Bạn có thể thiết lập tính năng này bằng setConsoleCallback()
.
Vì ngữ cảnh vẫn còn, bạn có thể tải mã lên và thực thi mã nhiều lần
trong thời gian hoạt động của 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);
Tất nhiên, các biến cũng có tính bền vững nên bạn có thể tiếp tục biến đoạn mã có:
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);
Ví dụ: đoạn mã hoàn chỉnh để phân bổ tất cả các đối tượng cần thiết và việc thực thi mã JavaScript có thể có dạng như sau:
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);
Bạn nên dùng try-with-resources để đảm bảo mọi thứ được phân bổ
các tài nguyên bị huỷ bỏ và không còn được sử dụng nữa. Đóng kết quả hộp cát
trong tất cả các lượt đánh giá đang chờ xử lý trong tất cả các thực thể JavaScriptIsolate
không đạt
bằng SandboxDeadException
. Khi quá trình đánh giá JavaScript gặp
lỗi thì hệ thống sẽ tạo JavaScriptException
. Tham chiếu đến các lớp con của lớp này
để biết các ngoại lệ cụ thể hơn.
Xử lý sự cố Hộp cát
Tất cả JavaScript đều được thực thi trong một quy trình hộp cát riêng biệt với quy trình chính của ứng dụng. Nếu mã JavaScript gây ra quá trình hộp cát này gặp sự cố, ví dụ: khi làm hết hạn mức bộ nhớ, màn hình chính của ứng dụng thì quy trình này sẽ không bị ảnh hưởng.
Sự cố hộp cát sẽ chấm dứt tất cả các vùng cách ly trong hộp cát đó. Nhiều nhất
dấu hiệu rõ ràng của điều này là tất cả các đánh giá sẽ bắt đầu thất bại với
IsolateTerminatedException
. Tuỳ thuộc vào tình huống, bạn có thể
ngoại lệ cụ thể như SandboxDeadException
hoặc
MemoryLimitExceededException
có thể được gửi.
Việc xử lý sự cố cho từng hoạt động đánh giá riêng lẻ không phải lúc nào cũng dễ thực hiện.
Hơn nữa, việc tách biệt có thể chấm dứt ngoài một yêu cầu rõ ràng
do các tác vụ trong nền hoặc hoạt động đánh giá ở những nền tảng khác. Tai nạn
logic xử lý có thể được tập trung bằng cách đính kèm một lệnh gọi lại sử dụng
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);
Các tính năng không bắt buộc của Hộp cát
Tuỳ thuộc vào phiên bản WebView cơ bản, phương thức triển khai hộp cát có thể phải tuân theo
các bộ tính năng khác nhau hiện có. Vì vậy, bạn cần phải truy vấn từng
bằng cách sử dụng JavaScriptSandbox.isFeatureSupported(...)
. Quan trọng là
để kiểm tra trạng thái của tính năng trước khi gọi các phương thức dựa vào những tính năng này.
Các phương thức JavaScriptIsolate
có thể không có ở mọi nơi
được chú thích bằng chú thích RequiresFeature
, giúp bạn dễ dàng nhận ra những
các lệnh gọi trong mã.
Tham số truyền
Nếu JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
là
nên các yêu cầu đánh giá gửi đến công cụ JavaScript sẽ không bị ràng buộc
theo giới hạn giao dịch liên kết. Nếu tính năng này không được hỗ trợ, tất cả dữ liệu để
JavaScriptEngine xảy ra thông qua giao dịch Binder. Các báo cáo chung
giới hạn kích thước giao dịch có thể áp dụng cho mọi cuộc gọi truyền dữ liệu hoặc
sẽ trả về dữ liệu.
Phản hồi luôn trả về dưới dạng Chuỗi và tuân theo Binder
giới hạn kích thước tối đa của giao dịch.
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
không phải là
được hỗ trợ. Các giá trị không phải chuỗi phải được chuyển đổi rõ ràng thành Chuỗi JavaScript
nếu không thì sẽ trả về một chuỗi trống. Nếu JS_FEATURE_PROMISE_RETURN
được hỗ trợ, mã JavaScript có thể trả về một Promise (Lời hứa)
đang phân giải một String
.
Để truyền các mảng byte lớn đến thực thể JavaScriptIsolate
, bạn
có thể sử dụng API provideNamedData(...)
. Việc sử dụng API này không bị ràng buộc bởi
hạn mức giao dịch Binder. Mỗi mảng byte phải được truyền bằng một giá trị duy nhất
giá trị nhận dạng không thể sử dụng lại.
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);
}
Đang chạy mã Wasm
Có thể chuyển mã WebAssembly (Wasm) bằng cách sử dụng provideNamedData(...)
API, sau đó được biên dịch và thực thi theo cách thông thường, như minh hoạ dưới đây.
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);
}
Tách JavaScript
Tất cả thực thể JavaScriptIsolate
đều độc lập với nhau và không
chia sẻ bất cứ điều gì. Đoạn mã sau đây dẫn đến
Hi from AAA!5
và
Uncaught Reference Error: a is not defined
vì thực thể "jsTwo
" không hiển thị các đối tượng được tạo trong đó
"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);
Hỗ trợ Kotlin
Để dùng thư viện Jetpack này với coroutine của Kotlin, hãy thêm một phần phụ thuộc vào
kotlinx-coroutines-guava
. Điều này cho phép tích hợp với
ListenableFuture
.
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}
Giờ đây, API thư viện Jetpack có thể được gọi từ phạm vi coroutine, như được minh hoạ dưới đây:
// 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
)
}
Thông số cấu hình
Khi yêu cầu một phiên bản môi trường tách biệt, bạn có thể điều chỉnh phiên bản đó
. Để tinh chỉnh cấu hình, hãy truyền
thực thể IsolateStartupParameters thành
JavaScriptSandbox.createIsolate(...)
.
Hiện tại, các tham số cho phép chỉ định kích thước vùng nhớ khối xếp tối đa và kích thước tối đa để đánh giá, các giá trị và lỗi sẽ trả về trong kết quả đánh giá.