執行 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 執行個體。一個 如果應用程式嘗試分配 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 String 類型;如果沒有,程式庫 API 會傳回空白值。 JavaScript 程式碼不應使用 return 關鍵字。如果沙箱 支援特定功能,額外的傳回類型 (例如 Promise 可能會解析為 String)。

此外,這個程式庫也支援評估 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);

建議使用嘗試使用資源,確保所有資源皆已分配 資源也會釋出且不再使用。關閉沙箱結果 (在所有 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 引擎的評估請求也沒有繫結 取決於繫結器交易限制如果不支援這項功能,則將資料複製到 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 隔離

所有 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
    )
}

設定參數

要求隔離環境執行個體時,您可以調整執行個體 此外還會從 0 自動調整資源配置 您完全不必調整資源調度設定如要調整設定,請傳送 IsolateStartupParameters 執行個體加入 JavaScriptSandbox.createIsolate(...)

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