Thực thi JavaScript và WebAssembly

Đá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)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ì hệ thống 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

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á.