JavaScript と WebAssembly を実行する

JavaScript の評価

Jetpack ライブラリ JavaScriptEngine を使用すると、 WebView のインスタンスを作成せずに JavaScript コードを評価できます。

非対話型の JavaScript 評価が必要なアプリケーションの場合は、 JavaScriptEngine ライブラリには、次のような利点があります。

  • WebView を割り当てる必要がないため、リソース消費量を抑える 作成します。

  • Service(WorkManager タスク)で実行できます。

  • オーバーヘッドの少ない複数の分離された環境により、アプリケーションで 複数の JavaScript スニペットを同時に実行できます。

  • API 呼び出しを使用して大量のデータを渡す機能。

基本的な使用方法

まず、JavaScriptSandbox のインスタンスを作成します。これは プロセス外 JavaScript エンジンに接続します。

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

サンドボックスのライフサイクルを このコンポーネントは JavaScript の評価が必要なコンポーネントです。

たとえば、サンドボックスをホストするコンポーネントは Activity または Service。単一の Service を使用して JavaScript 評価をカプセル化できます。 すべてのアプリケーション コンポーネントが対象になります。

割り当てが適正であるため、JavaScriptSandbox インスタンスを維持する 高価ですJavaScriptSandbox インスタンスは、アプリケーションごとに 1 つだけ許可されます。「 IllegalStateException は、アプリがスペースを割り当てようとしたときにスローされます。 2 番目の JavaScriptSandbox インスタンス。ただし、複数の実行環境が 必要な場合は、複数の JavaScriptIsolate インスタンスを割り当てることができます。

不要になったらサンドボックス インスタンスを閉じてリソースを解放します。「 JavaScriptSandbox インスタンスは、AutoCloseable インターフェースを実装します。 シンプルなブロックのユースケースで try-with-resources を使用できます。 または、JavaScriptSandbox インスタンスのライフサイクルが アクティビティの onStop() コールバックで閉じるか、 Service の onDestroy() の間:

jsSandbox.close();

JavaScriptIsolate インスタンスは、実行のためのコンテキストを表します。 使用できます。必要に応じて割り当てることができるため、セキュリティが脆弱 オリジンが異なるスクリプトの境界線、または 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 コード内の最後の式の結果を返します。必要があります。 JavaScript String 型の値。それ以外の場合、ライブラリ API は空の値を返します。 JavaScript コードでは return キーワードを使用しないでください。サンドボックスが 特定の機能、追加の戻り値の型(たとえば、Promise String に解決されるものなど)が考えられます。

このライブラリでは、Python 形式で記述されたスクリプトの評価も AssetFileDescriptor または ParcelFileDescriptor。詳しくは、 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);

オプションのサンドボックス機能

基盤となる WebView のバージョンによっては、サンドボックスの実装に さまざまな機能が用意されています。そのため、それぞれの要求に応えるために、 JavaScriptSandbox.isFeatureSupported(...) を使用します。大事なこと を使用して、これらの機能に依存するメソッドを呼び出す前に、機能のステータスを確認することができます。

地域によっては使用できない可能性のある JavaScriptIsolate メソッドがある RequiresFeature アノテーションが付いているため、簡単に特定できます。 必要があります。

パラメータの引き渡し

JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT が次の場合: JavaScript エンジンに送信される評価リクエストは バインダー トランザクションの上限によって制限されます。この機能がサポートされていない場合、 JavaScriptEngine は Binder トランザクションを通じて発生します。全般 トランザクション サイズ制限は、 がデータを返します。

応答は常に文字列として返され、Binder の対象である トランザクションの最大サイズが 次を含まない: JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT サポートされません。文字列以外の値は、明示的に JavaScript 文字列に変換する必要があります それ以外の場合は空の文字列が返されます。JS_FEATURE_PROMISE_RETURN の場合 サポートされている場合、JavaScript コードは代わりに Promise を返す Stringに解決されます

大きなバイト配列を JavaScriptIsolate インスタンスに渡すには、次のようにします。 provideNamedData(...) API を使用できます。この API の使用は、 Binder トランザクションの上限が適用されます。各バイト配列は、一意の ID を使用して 再利用できません。

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 コードの実行

WebAssembly(Wasm)コードは、provideNamedData(...) を使用して渡すことができます。 その後、以下のように、通常の方法でコンパイルして実行されます。

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);
}

JavaScriptIsolate 分離

すべての 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
    )
}

構成パラメータ

隔離された環境インスタンスをリクエストする場合、そのインスタンスを できます。構成を調整するには、 IsolateStartupParameters のインスタンスを分離し、 JavaScriptSandbox.createIsolate(...)

現在、パラメータを使用して最大ヒープサイズと最大サイズを指定できます。 評価の戻り値とエラーを確認できます