JavaScript-Evaluierung
Die Jetpack-Bibliothek JavaScriptEngine bietet einer Anwendung die Möglichkeit, JavaScript-Code auszuwerten, ohne eine WebView-Instanz zu erstellen.
Für Anwendungen, die eine nicht interaktive JavaScript-Evaluierung erfordern, bietet die Verwendung der JavaScriptEngine-Bibliothek folgende Vorteile:
Geringerer Ressourcenverbrauch, da keine WebView-Instanz zugewiesen werden muss.
Kann in einem Dienst (WorkManager-Aufgabe) ausgeführt werden.
Mehrere isolierte Umgebungen mit geringem Aufwand, sodass die Anwendung mehrere JavaScript-Snippets gleichzeitig ausführen kann
Möglichkeit, große Datenmengen mithilfe eines API-Aufrufs zu übergeben.
Grundlegende Verwendung
Erstellen Sie zuerst eine Instanz von JavaScriptSandbox
. Dies stellt eine Verbindung zur Out-of-Process-JavaScript-Engine dar.
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
Es wird empfohlen, den Lebenszyklus der Sandbox auf den Lebenszyklus der Komponente abzustimmen, für die eine JavaScript-Bewertung erforderlich ist.
Eine Komponente zum Hosten der Sandbox kann beispielsweise ein Activity
oder ein Service
sein. Eine einzelne Service
kann verwendet werden, um die JavaScript-Auswertung für alle Anwendungskomponenten zu kapseln.
Behalten Sie die Instanz JavaScriptSandbox
bei, da ihre Zuweisung recht teuer ist. Es ist nur eine JavaScriptSandbox
-Instanz pro Anwendung zulässig. Ein IllegalStateException
wird ausgelöst, wenn eine Anwendung versucht, eine zweite JavaScriptSandbox
-Instanz zuzuweisen. Wenn jedoch mehrere Ausführungsumgebungen erforderlich sind, können mehrere JavaScriptIsolate
-Instanzen zugewiesen werden.
Wenn sie nicht mehr verwendet wird, schließen Sie die Sandbox-Instanz, um Ressourcen freizugeben. Die Instanz JavaScriptSandbox
implementiert eine AutoCloseable
-Schnittstelle, die die Nutzung von „Ausprobieren“ mit Ressourcen für einfache blockierende Anwendungsfälle ermöglicht.
Alternativ können Sie dafür sorgen, dass der Lebenszyklus der JavaScriptSandbox
-Instanz von der Hosting-Komponente verwaltet wird. Schließen Sie dazu sie im onStop()
-Callback für eine Aktivität oder während onDestroy()
für einen Dienst:
jsSandbox.close();
Eine JavaScriptIsolate
-Instanz stellt einen Kontext zum Ausführen von JavaScript-Code dar. Sie können bei Bedarf zugewiesen werden, um schwache Sicherheitsgrenzen für Skripts unterschiedlicher Herkunft bereitzustellen oder die gleichzeitige JavaScript-Ausführung zu ermöglichen, da JavaScript von Natur aus Single-Threaded ist. Nachfolgende Aufrufe derselben Instanz haben denselben Status. Daher ist es möglich, zuerst einige Daten zu erstellen und später in derselben Instanz von JavaScriptIsolate
zu verarbeiten.
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
Geben Sie JavaScriptIsolate
explizit durch Aufrufen der Methode close()
frei.
Das Schließen einer isolierten Instanz, auf der JavaScript-Code ausgeführt wird (mit einem unvollständigen Future
), führt zu einem IsolateTerminatedException
. Das Isolations-Tool wird anschließend im Hintergrund bereinigt, wenn die Implementierung JS_FEATURE_ISOLATE_TERMINATION
unterstützt, wie im Abschnitt Umgang mit Sandbox-Abstürzen weiter unten auf dieser Seite beschrieben. Andernfalls wird die Bereinigung verschoben, bis alle ausstehenden Bewertungen abgeschlossen sind oder die Sandbox geschlossen ist.
Eine Anwendung kann eine JavaScriptIsolate
-Instanz von jedem Thread aus erstellen und darauf zugreifen.
Jetzt kann die Anwendung JavaScript-Code ausführen:
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);
Dasselbe JavaScript-Snippet ist gut formatiert:
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);
Das Code-Snippet wird als String
übergeben und das Ergebnis als String
übergeben.
Wenn Sie evaluateJavaScriptAsync()
aufrufen, wird das ausgewertete Ergebnis des letzten Ausdrucks im JavaScript-Code zurückgegeben. Sie muss vom Typ String
in JavaScript sein. Andernfalls gibt die Library API einen leeren Wert zurück.
Der JavaScript-Code sollte kein return
-Keyword enthalten. Wenn die Sandbox bestimmte Funktionen unterstützt, sind unter Umständen zusätzliche Rückgabetypen möglich (z. B. ein Promise
, das zu einem String
aufgelöst wird).
Die Bibliothek unterstützt auch die Bewertung von Skripts in Form eines AssetFileDescriptor
- oder ParcelFileDescriptor
-Skripts. Weitere Informationen finden Sie unter evaluateJavaScriptAsync(AssetFileDescriptor)
und evaluateJavaScriptAsync(ParcelFileDescriptor)
.
Diese APIs eignen sich besser für die Auswertung anhand einer Datei auf einem Laufwerk oder in Anwendungsverzeichnissen.
Die Bibliothek unterstützt auch das Logging in der Konsole, das für die Fehlerbehebung verwendet werden kann. Dies kann mit setConsoleCallback()
eingerichtet werden.
Da der Kontext bestehen bleibt, können Sie Code hochladen und während der Lebensdauer von JavaScriptIsolate
mehrmals ausführen:
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);
Natürlich sind die Variablen auch persistent, sodass Sie das vorherige Snippet so fortsetzen können:
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);
Das vollständige Snippet zum Zuweisen aller erforderlichen Objekte und zum Ausführen eines JavaScript-Codes könnte beispielsweise so aussehen:
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);
Es empfiehlt sich, „try-with-resources“ zu verwenden, um sicherzustellen, dass alle zugewiesenen Ressourcen freigegeben und nicht mehr verwendet werden. Wenn Sie die Sandbox schließen, schlagen alle ausstehenden Bewertungen in allen JavaScriptIsolate
-Instanzen fehl und die Fehlermeldung SandboxDeadException
wird zurückgegeben. Wenn bei der JavaScript-Bewertung ein Fehler auftritt, wird ein JavaScriptException
erstellt. Spezifischere Ausnahmen finden Sie in den zugehörigen abgeleiteten Klassen.
Sandbox-Abstürze verarbeiten
JavaScript wird in einem separaten Sandbox-Prozess außerhalb des Hauptprozesses Ihrer Anwendung ausgeführt. Wenn der JavaScript-Code dazu führt, dass dieser in der Sandbox ausgeführte Prozess abstürzt, z. B. weil ein Arbeitsspeicherlimit erschöpft ist, ist der Hauptprozess der Anwendung nicht betroffen.
Ein Sandbox-Absturz führt dazu, dass alle Isolationen in dieser Sandbox beendet werden. Das offensichtlichste Anzeichen dafür ist, dass alle Bewertungen mit IsolateTerminatedException
fehlschlagen. Je nach den Umständen werden spezifischere Ausnahmen wie SandboxDeadException
oder MemoryLimitExceededException
ausgelöst.
Der Umgang mit Abstürzen bei jeder einzelnen Bewertung ist nicht immer praktikabel.
Außerdem kann ein Isolationsprozess außerhalb einer explizit angeforderten Bewertung aufgrund von Hintergrundaufgaben oder Bewertungen in anderen Isolationen beendet werden. Die Logik für die Absturzbehandlung kann zentralisiert werden, indem mit JavaScriptIsolate.addOnTerminatedCallback()
ein Callback angehängt wird.
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);
Optionale Sandbox-Funktionen
Je nach zugrunde liegender WebView-Version stehen für eine Sandbox-Implementierung möglicherweise unterschiedliche Funktionen zur Verfügung. Daher müssen Sie jedes erforderliche Feature mit JavaScriptSandbox.isFeatureSupported(...)
abfragen. Es ist wichtig, den Featurestatus zu prüfen, bevor Methoden aufgerufen werden, die diese Features nutzen.
JavaScriptIsolate
-Methoden, die möglicherweise nicht überall verfügbar sind, werden mit der Annotation RequiresFeature
versehen, sodass diese Aufrufe im Code leichter zu erkennen sind.
Parameter übergeben
Wenn JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
unterstützt wird, sind die an die JavaScript-Engine gesendeten Bewertungsanfragen nicht an die Transaktionslimits der Binder gebunden. Wenn die Funktion nicht unterstützt wird, werden alle Daten an die JavaScriptEngine über eine Binder-Transaktion ausgeführt. Die allgemeine Größenbeschränkung für Transaktionen gilt für jeden Aufruf, bei dem Daten übergeben oder zurückgegeben werden.
Die Antwort wird immer als String zurückgegeben und unterliegt der Größenbeschränkung für Binder-Transaktionen, wenn JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
nicht unterstützt wird. Nicht-String-Werte müssen explizit in einen JavaScript-String konvertiert werden. Andernfalls wird ein leerer String zurückgegeben. Wenn das JS_FEATURE_PROMISE_RETURN
-Feature unterstützt wird, kann JavaScript-Code alternativ ein Promise-Objekt an ein String
zurückgeben.
Zum Übertragen großer Byte-Arrays an die JavaScriptIsolate
-Instanz können Sie die provideNamedData(...)
API verwenden. Die Nutzung dieser API ist nicht an die Binder-Transaktionslimits gebunden. Jedes Bytearray muss mit einer eindeutigen Kennzeichnung übergeben werden, die nicht wiederverwendet werden kann.
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-Code ausführen
WebAssembly-Code (Wasm) kann mit der provideNamedData(...)
API übergeben und dann wie unten gezeigt kompiliert und ausgeführt werden.
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-Isolierung
Alle JavaScriptIsolate
-Instanzen sind unabhängig voneinander und haben nichts gemeinsam. Das folgende Snippet führt zu
Hi from AAA!5
und
Uncaught Reference Error: a is not defined
da die Instanz „jsTwo
“ keine Sichtbarkeit der in „jsOne
“ erstellten Objekte hat.
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-Unterstützung
Wenn Sie diese Jetpack-Bibliothek mit Kotlin-Coroutinen verwenden möchten, fügen Sie kotlinx-coroutines-guava
eine Abhängigkeit hinzu. Dies ermöglicht die Integration mit ListenableFuture
.
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}
Die Jetpack Library APIs können jetzt wie unten gezeigt aus einem Koroutinenbereich aufgerufen werden:
// 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
)
}
Konfigurationsparameter
Wenn Sie eine Instanz einer isolierten Umgebung anfordern, können Sie deren Konfiguration optimieren. Übergeben Sie die Instanz IsolateStartupParameters an JavaScriptSandbox.createIsolate(...)
, um die Konfiguration zu optimieren.
Derzeit können mit Parametern die maximale Heap-Größe und die maximale Größe für Rückgabewerte und Fehler bei der Auswertung angegeben werden.