JavaScript の評価
Jetpack ライブラリの JavaScriptEngine は、WebView インスタンスを作成せずにアプリが JavaScript コードを評価できるようにします。
インタラクティブでない JavaScript 評価が必要なアプリケーションの場合、JavaScriptEngine ライブラリを使用すると次のメリットがあります。
WebView を割り当てる必要がないため、リソース消費量を抑える 作成します。
サービス(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
。状況によっては、
特定の例外(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
がサポートされていない場合、Binder トランザクションの最大サイズの上限が適用されます。文字列以外の値は、明示的に 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(...)
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);
}
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(...)
。
現在、パラメータでは、ヒープサイズの最大値と、評価の戻り値とエラーの最大サイズを指定できます。