執行 JavaScript 和 WebAssembly

JavaScript 評估

Jetpack 程式庫 JavaScriptEngine 提供一種方法,可讓應用程式在不建立 WebView 例項的情況下評估 JavaScript 程式碼。

如果應用程式需要非互動式 JavaScript 評估,請使用 JavaScriptEngine 程式庫具有以下優點:

  • 降低資源用量,因為不需要分配 WebView 執行個體。

  • 可在 Service (WorkManager 工作) 中完成。

  • 多個低負擔的隔離環境,可讓應用程式同時執行多個 JavaScript 程式碼片段。

  • 可透過 API 呼叫傳遞大量資料。

基本用法

首先,請建立 JavaScriptSandbox 的執行個體。代表創造的 連線至獨立處理程序 JavaScript 引擎。

ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
               JavaScriptSandbox.createConnectedInstanceAsync(context);

建議您將沙箱的生命週期與需要 JavaScript 評估的元件生命週期保持一致。

舉例來說,代管沙箱的元件可能是 ActivityService。單一 Service 可能會用於封裝所有應用程式元件的 JavaScript 評估。

請維護 JavaScriptSandbox 例項,因為其分配作業相當昂貴。每個應用程式只能使用一個 JavaScriptSandbox 例項。當應用程式嘗試配置第二個 JavaScriptSandbox 例項時,系統會擲回 IllegalStateException。不過,如果需要多個執行環境,則可分配多個 JavaScriptIsolate 例項。

不再使用時,請關閉沙箱執行個體以釋出資源。 JavaScriptSandbox 執行個體會實作 AutoCloseable 介面。 可讓簡單的封鎖用途使用 try-with-resources。 或者,請確保 JavaScriptSandbox 例項生命週期由代管元件管理,在活動的 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 String 類型,否則程式庫 API 會傳回空值。JavaScript 程式碼不應使用 return 關鍵字。如果沙箱支援特定功能,可能會有其他傳回類型 (例如解析為 StringPromise)。

這個程式庫也支援以 AssetFileDescriptorParcelFileDescriptor 形式評估指令碼。詳情請見 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。視情況而定,系統可能會擲回更具體的例外狀況,例如 SandboxDeadExceptionMemoryLimitExceededException

為每項個別評估處理當機問題並不一定可行。此外,由於其他隔離區中的背景工作或評估作業,隔離區可能會在明確要求評估以外的情況下終止。您可以使用 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);

選用沙箱功能

Sandbox 實作項目可能會提供不同的功能組合,這取決於基礎 WebView 版本。因此您需要查詢 功能 (JavaScriptSandbox.isFeatureSupported(...))。因此,請務必先檢查功能狀態,再呼叫依賴這些功能的方法。

JavaScriptIsolate 方法可能無法在所有地方使用,因此我們會加上 RequiresFeature 註解,方便您在程式碼中找出這些呼叫。

傳送參數

如果支援 JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT,則傳送至 JavaScript 引擎的評估要求不會受綁定器交易限制的約束。如果不支援這項功能,則將資料複製到 JavaScriptEngine 是透過 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);
}

JavaScript Isolate 分隔

所有 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(...)

目前的參數可讓您指定堆積大小上限和大小上限 傳回值和錯誤