Đá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 vì không cần phân bổ thực thể WebView.
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 đồng thời nhiều đoạn mã JavaScript.
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 điều 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ụ: một 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á tốn kém. Mỗi ứng dụng chỉ được phép có một thực thể 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ãy đóng thực thể hộp cát để giải phóng tài nguyên. Thực thể JavaScriptSandbox
triển khai giao diện AutoCloseable
, cho phép sử dụng try-with-resources cho 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();
Một thực thể JavaScriptIsolate
đại diện cho một 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. Các lệnh gọi tiếp theo đến cùng một thực thể sẽ có cùng trạng thái, do đó, bạn có thể tạo một số dữ liệu trước rồi xử lý dữ liệu đó sau trong cùng một thực thể của JavaScriptIsolate
.
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
Phát hành 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
.
Xin lưu ý rằng việc gọi evaluateJavaScriptAsync()
sẽ trả về kết quả đánh giá của biểu thức cuối cùng trong mã JavaScript. Đây phải là loại String
JavaScript; nếu không, API thư viện sẽ trả về một giá trị trống.
Mã JavaScript không được sử dụng từ khoá 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 ở dạng AssetFileDescriptor
hoặc ParcelFileDescriptor
. Hãy xem evaluateJavaScriptAsync(AssetFileDescriptor)
và evaluateJavaScriptAsync(ParcelFileDescriptor)
để biết thêm thông tin 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 tồn tại, vì vậy, bạn có thể tiếp tục đoạn mã trước đó bằng:
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 sử dụng try-with-resources để đảm bảo tất cả tài nguyên được phân bổ đều được giải phóng và không còn được sử dụng nữa. Việc đóng hộp cát sẽ dẫn đến kết quả là tất cả các lượt đánh giá đang chờ xử lý trong tất cả các thực thể JavaScriptIsolate
đều không thành công với SandboxDeadException
. Khi quá trình đánh giá JavaScript gặp
lỗi thì hệ thống sẽ tạo JavaScriptException
. Hãy tham khảo các lớp con của lớp này để biết thêm các trường hợp ngoại lệ cụ thể.
Xử lý sự cố trong 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 khiến quy trình trong hộp cát này gặp sự cố, chẳng hạn như do hết giới hạn bộ nhớ, thì quy trình chính của ứng dụng 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 trường hợp, các ngoại lệ cụ thể hơn 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, một vùng chứa biệt lập có thể chấm dứt bên ngoài một quá trình đánh giá được yêu cầu rõ ràng do các tác vụ hoặc quá trình đánh giá ở chế độ nền trong các vùng chứa biệt lập khác. Bạn có thể tập trung logic xử lý sự cố bằng cách đính kèm lệnh gọi lại bằ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 truy vấn từng tính năng bắt buộc bằng JavaScriptSandbox.isFeatureSupported(...)
. Điều quan trọng là phải kiểm tra trạng thái tính năng trước khi gọi các phương thức dựa vào các 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 đến JavaScriptEngine sẽ 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 được trả về dưới dạng Chuỗi và tuân theo giới hạn kích thước tối đa của giao dịch Binder nếu JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
không được hỗ trợ. Bạn phải chuyển đổi rõ ràng các giá trị không phải chuỗi thành Chuỗi JavaScript, nếu không hệ thống sẽ trả về một chuỗi trống. Nếu tính năng JS_FEATURE_PROMISE_RETURN
được hỗ trợ, mã JavaScript có thể trả về một Lời hứa phân giải thành 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 giới hạn giao dịch của Binder. Mỗi mảng byte phải được truyền bằng một giá trị nhận dạng duy nhất 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);
}
Chạy mã Wasm
Bạn có thể truyền mã WebAssembly (Wasm) bằng API provideNamedData(...)
, sau đó biên dịch và thực thi theo cách thông thường, như minh hoạ bên dưới.
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);
}
Phân tách JavaScriptIsolate
Tất cả các thực thể JavaScriptIsolate
đều độc lập với nhau và không chia sẻ bất kỳ nội dung nào. Đ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 có chế độ 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
Để sử dụng thư viện Jetpack này với coroutine 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, bạn có thể gọi các API thư viện Jetpack từ phạm vi coroutine, như minh hoạ bên dưới:
// 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 thực thể môi trường tách biệt, bạn có thể điều chỉnh cấu hình của thực thể đó. Để 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 cho các giá trị trả về và lỗi đánh giá.