JavaScript-Evaluierung
Die Jetpack-Bibliothek JavaScriptEngine bietet einer Anwendung die Möglichkeit, JavaScript-Code evaluieren, ohne eine WebView-Instanz zu erstellen.
Verwenden Sie für Anwendungen, die eine nicht interaktive JavaScript-Auswertung erfordern, den Die JavaScriptEngine-Bibliothek bietet folgende Vorteile:
Geringerer Ressourcenverbrauch, da keine WebView-Instanz zugewiesen werden muss.
Kann in einem Service (WorkManager-Aufgabe) ausgeführt werden.
Mehrere isolierte Umgebungen mit geringem Aufwand, sodass die Anwendung mehrere JavaScript-Snippets gleichzeitig ausführen.
Fähigkeit, 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.
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
Es empfiehlt sich, den Lebenszyklus der Sandbox auf den Lebenszyklus der Komponente, die JavaScript-Auswertung benötigt.
Beispielsweise kann eine Komponente, die die Sandbox hostet, ein Activity
oder ein
Service
. Es kann eine einzelne Service
verwendet werden, um die JavaScript-Auswertung zu kapseln
für alle Anwendungskomponenten.
Behalten Sie die JavaScriptSandbox
-Instanz bei, da die Zuweisung relativ teuer ist. Pro Anwendung ist nur eine JavaScriptSandbox
-Instanz zulässig. Eine
IllegalStateException
wird ausgelöst, wenn eine Anwendung versucht, einen
zweite JavaScriptSandbox
-Instanz. Wenn jedoch mehrere Ausführungsumgebungen erforderlich sind, können mehrere JavaScriptIsolate
-Instanzen zugewiesen werden.
Wenn Sie die Sandbox nicht mehr verwenden, schließen Sie die Instanz, um Ressourcen freizugeben. Die
Die JavaScriptSandbox
-Instanz implementiert eine AutoCloseable
-Schnittstelle, die
ermöglicht das Testen mit Ressourcen für einfache blockierende Anwendungsfälle.
Alternativ kannst du dafür sorgen, dass der JavaScriptSandbox
-Instanzlebenszyklus von der Hostingkomponente verwaltet wird, indem du sie im onStop()
-Callback für eine Aktivität oder während onDestroy()
für einen Dienst schließt:
jsSandbox.close();
Eine JavaScriptIsolate
-Instanz stellt einen Kontext zum Ausführen
JavaScript-Code. Sie können bei Bedarf zugewiesen werden, um schwache Sicherheitsgrenzen für Scripts unterschiedlicher Herkunft zu schaffen oder die gleichzeitige Ausführung von JavaScript zu ermöglichen, da JavaScript von Natur aus ein einzelner Thread ist. Nachfolgende Aufrufe von
dieselbe Instanz den gleichen Status hat, daher ist es möglich, Daten zu erstellen,
und später in derselben Instanz von JavaScriptIsolate
verarbeiten.
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
Geben Sie JavaScriptIsolate
explizit frei, indem Sie die zugehörige Methode close()
aufrufen.
Wenn eine isolierte Instanz mit JavaScript-Code geschlossen wird, der eine unvollständige Future
enthält, führt dies zu einer IsolateTerminatedException
. Das Isolate wird anschließend im Hintergrund beseitigt, wenn die Implementierung JS_FEATURE_ISOLATE_TERMINATION
unterstützt, wie im Abschnitt Sandbox-Abstürze beheben weiter unten auf dieser Seite beschrieben. Andernfalls wird die Bereinigung verschoben, bis alle ausstehenden Bewertungen abgeschlossen sind oder die Sandbox geschlossen wird.
Eine Anwendung kann eine JavaScriptIsolate
-Instanz erstellen und auf sie zugreifen aus
jedem Thread.
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)";
Listen<ableFu>tureString resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.
SECONDS);
Das gleiche JavaScript-Snippet, schön 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
geliefert.
Hinweis: Wenn Sie evaluateJavaScriptAsync()
aufrufen, wird das ausgewertete Ergebnis des letzten Ausdrucks im JavaScript-Code zurückgegeben. Dieser muss vom JavaScript-Typ String
sein. Andernfalls gibt die Bibliothek API einen leeren Wert zurück.
Im JavaScript-Code darf kein return
-Keyword verwendet werden. Wenn die Sandbox
bestimmte Funktionen und zusätzliche Rückgabetypen unterstützt (z. B. ein Promise
)
der zu String
aufgelöst wird, möglich sein.
Die Bibliothek unterstützt auch die Auswertung von Scripts im Format AssetFileDescriptor
oder ParcelFileDescriptor
. Weitere Informationen finden Sie unter
evaluateJavaScriptAsync(AssetFileDescriptor)
und
evaluateJavaScriptAsync(ParcelFileDescriptor)
.
Diese APIs eignen sich besser für die Auswertung aus einer Datei auf dem Laufwerk oder in App-Verzeichnissen.
Die Bibliothek unterstützt auch Console-Logging, 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 mehrmals ausführen
während der Lebensdauer von JavaScriptIsolate
:
String jsFunction = "function sum(a, b) { let r = a + b; return r.toString(); }";
Listen<ableFu>tureString func = js.evaluateJavaScriptAsync(jsFunction);
String twoPlusThreeCode = "let five = sum(2, 3); five&quo<t;;
Li>stenableFutureString r1 = Futures.transformAsync(>func,
input - js.evaluateJavaScriptAsync(twoPlusThreeCode)
, executor);
String twoPlusThree = r1.get(5, TimeUnit.SECONDS);
String fourPlusFiveCode = "sum(4, parseInt(<five))>";
ListenableFutureString r2 = Futures.trans>formAsync(func,
input - js.evaluateJavaScriptAsync(fourPlusFiveCode)
, executor);
String fourPlusFive =
r2.get(5, TimeUnit.SECONDS);
Natürlich sind die Variablen auch persistent, sodass Sie Snippet mit:
String defineResult = "let result = sum(11, 22);";
Listen<ableFu>tureString r3 = Futures.transformAsync(func,
> input - js.evaluateJavaScriptAsync(defineResult)
, executor);
String unused = r3.get(5, TimeUnit.SECONDS);
String obtainValue = "result&quo<t;;
Li>stenableFutureString 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 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 Fu>tureCallbackString() {
@Override
public void onSuccess(String result) {
text.append(result);
}
@Override
public void onFailure(Throwable t) {
text.append(t.getMessage());
}
},
mai
nThreadExecutor);
Wir empfehlen, try-with-resources zu verwenden, damit alle zugewiesenen Ressourcen freigegeben und nicht mehr verwendet werden. Sandbox-Ergebnisse schließen
in allen ausstehenden Bewertungen in allen JavaScriptIsolate
Instanzen fehlschlagen
mit einem SandboxDeadException
. Wenn bei der JavaScript-Bewertung ein Fehler auftritt, wird ein JavaScriptException
erstellt. Weitere Ausnahmen finden Sie in den untergeordneten Klassen.
Umgang mit Sandbox-Abstürzen
Das gesamte JavaScript wird in einem separaten Sandbox-Prozess ausgeführt, der vom Hauptprozess Ihrer Anwendung getrennt ist. Wenn der JavaScript-Code dazu führt, dass dieser sandboxed-Prozess abstürzt, z. B. weil ein Speicherlimit überschritten wird, ist der Hauptprozess der Anwendung davon nicht betroffen.
Bei einem Sandbox-Absturz werden alle Isolate in dieser Sandbox beendet. Die meisten
offensichtliches Symptom hierfür ist, dass alle Bewertungen
IsolateTerminatedException
Je nach Umständen können auch spezifischere Ausnahmen wie SandboxDeadException
oder MemoryLimitExceededException
geworfen werden.
Es ist nicht immer praktikabel, Abstürze für jede einzelne Bewertung zu behandeln.
Darüber hinaus kann ein Isolator außerhalb eines explizit angeforderten
einer Bewertung aufgrund von Hintergrundaufgaben
oder Bewertungen in anderen Isolierungen. Die Logik zur Crashbehandlung kann zentralisiert werden, indem Sie mit JavaScriptIsolate.addOnTerminatedCallback()
einen Callback anhängen.
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 sind für eine Sandbox-Implementierung möglicherweise unterschiedliche Funktionen verfügbar. Daher müssen alle erforderlichen Funktionen mit JavaScriptSandbox.isFeatureSupported(...)
abgefragt werden. Es ist wichtig, den Funktionsstatus zu prüfen, bevor Methoden aufgerufen werden, die auf diesen Funktionen basieren.
JavaScriptIsolate
-Methoden, die möglicherweise nicht überall verfügbar sind, sind mit der Anmerkung RequiresFeature
versehen, damit diese Aufrufe im Code leichter zu erkennen sind.
Parameter übergeben
Wenn JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
gleich
unterstützt, sind die an die JavaScript-Engine gesendeten Bewertungsanfragen nicht gebunden
binder-Transaktionslimits einzuhalten. Wenn die Funktion nicht unterstützt wird, werden alle Daten an die JavaScript-Engine über eine Binder-Transaktion gesendet. Das allgemeine
Transaktionsgrößenlimit gilt für jeden Aufruf, bei dem Daten oder
gibt Daten zurück.
Die Antwort wird immer als String zurückgegeben und unterliegt der maximalen Größe einer Binder-Transaktion, wenn JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
nicht unterstützt wird. Nicht-Stringwerte müssen explizit in einen JavaScript-String konvertiert werden, andernfalls wird ein leerer String zurückgegeben. Wenn die Funktion JS_FEATURE_PROMISE_RETURN
unterstützt wird, kann JavaScript-Code alternativ ein Promise zurückgeben, das in einen String
aufgelöst wird.
Für die Übergabe großer Byte-Arrays an die JavaScriptIsolate
-Instanz
können die provideNamedData(...)
API verwenden. Die Nutzung dieser API unterliegt nicht den Transaktionslimits von Binder. Jedes Byte-Array muss mit einer eindeutigen Kennung ü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(va<lue));> });";
ListenableFutureString msg = js.evaluateJavaScriptAsync(jsCode);
String respo
nse = msg.get(5, TimeUnit.SECONDS);
}
Wasm-Code ausführen
WebAssembly-Code (Wasm) kann mit der provideNamedData(...)
übergeben werden.
API kompiliert und dann wie unten gezeigt kompiliert und ausgeführt wird.
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 = "(asyn>c ()={" +
"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))
.transfor>m(this::println, mainThreadExecutor)
.
catching(Throwable.class, e - println(e.getMessage()), mainThreadExecutor);
}
JavaScriptIsolate-Trennung
Alle JavaScriptIsolate
-Instanzen sind unabhängig voneinander und haben keine gemeinsamen Elemente. Das folgende Snippet führt zu
Hi from AAA!5
und
Uncaught Reference Error: a is not defined
weil die Instanz „jsTwo
“ keine Sichtbarkeit für die 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)
.c>atching(Throwable.class, e - println(e.getMessage()), mainThreadExecutor);
FluentFuture.from(jsTwo.evaluateJavaScriptAsync(jsCodeTwo))
.transform(this::println, mainThreadExecutor)
.c>atching(Throwable.class, e - println(e.getMessa
ge()), mainThreadExecutor);
Kotlin-Unterstützung
Wenn Sie diese Jetpack-Bibliothek mit Kotlin-Coroutinen verwenden möchten, fügen Sie eine Abhängigkeit zu kotlinx-coroutines-guava
hinzu. Dies ermöglicht die Integration mit
ListenableFuture
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.
0"
}
Die APIs der Jetpack-Bibliothek können jetzt aus einem Coroutinen-Scope aufgerufen werden, wie unten gezeigt:
// 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.a<ddCall>backString(
resultFuture, object : Futu<reCallb>ackString? {
override fun onSuccess(result: String?) {
textBox.text = result
}
override fun onFailure(t: Throwable) {
// Handle errors
}
},
mainExecuto
r
)
}
Konfigurationsparameter
Wenn Sie eine Instanz einer isolierten Umgebung anfordern, können Sie die Konfiguration anpassen. Um die Konfiguration zu optimieren, übergeben Sie die
IsolateStartupParameters in
JavaScriptSandbox.createIsolate(...)
Derzeit können Sie mithilfe von Parametern die maximale Heap-Größe und die maximale Größe für Rückgabewerte und Fehler der Auswertung angeben.