JavaScript und WebAssembly ausführen

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 zugewiesen werden muss Instanz.

  • 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 Nutzung

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.

JavaScriptSandbox-Instanz beibehalten, da ihre Zuweisung recht erfolgt teuer sein. 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 nicht mehr verwendet wird, schließen Sie die Sandbox-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 können Sie dafür sorgen, dass der Lebenszyklus der JavaScriptSandbox-Instanz von der der Hostingkomponente und schließen 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 JavaScript-Code. Sie können bei Bedarf zugewiesen werden und bieten so schwache Sicherheit. Grenzen für Skripts unterschiedlichen Ursprungs oder die Aktivierung gleichzeitiger JavaScripts ausgeführt wird, da JavaScript von Natur aus Single-Threaded ist. Nachfolgende Aufrufe von dieselbe Instanz haben den gleichen Status, 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. Schließen einer Isolationsinstanz, auf der JavaScript-Code ausgeführt wird (ein unvollständiges Future) führt zu einem IsolateTerminatedException. Die 'Isolierung' wird anschließend im Hintergrund bereinigt, wenn die Implementierung unterstützt JS_FEATURE_ISOLATE_TERMINATION, wie in den Umgang mit Sandbox-Abstürzen weiter unten. Seite. Andernfalls wird die Bereinigung verschoben, bis alle ausstehenden Bewertungen abgeschlossen sind abgeschlossen 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)";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);

Dasselbe JavaScript-Snippet 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 geliefert. Beachten Sie, dass das Aufrufen von evaluateJavaScriptAsync() die bewertete Ergebnis des letzten Ausdrucks im JavaScript-Code. Dies muss des JavaScript-Typs String; Andernfalls gibt die Library 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 Skripts in Form eines AssetFileDescriptor oder ParcelFileDescriptor. Weitere Informationen finden Sie unter evaluateJavaScriptAsync(AssetFileDescriptor) und evaluateJavaScriptAsync(ParcelFileDescriptor). Diese APIs eignen sich besser für die Auswertung über eine Datei auf der Festplatte oder in der App. Verzeichnisse enthalten.

Die Bibliothek unterstützt auch Konsolen-Logging, das für die Fehlerbehebung verwendet werden kann zu verstehen. 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(); }";
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 auch die Variablen persistent, sodass Sie Snippet mit:

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

Zum Beispiel das komplette Snippet für die Zuordnung aller notwendigen Objekte und kann das Ausführen eines JavaScript-Codes wie folgt 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);

Wir empfehlen Ihnen, Ressourcen zum Testen zu nutzen, um sicherzustellen, dass alle 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 Fehler ist, wird ein JavaScriptException erstellt. Sehen Sie sich die zugehörigen abgeleiteten Klassen an. für spezifischere Ausnahmen.

Umgang mit Sandbox-Abstürzen

Das gesamte JavaScript wird in einem separaten Sandbox-Prozess außerhalb der Hauptprozess der Anwendung. Wenn der JavaScript-Code diesen Sandbox-Prozess verursacht zum Absturz gerät, z. B. durch Überschreitung eines Speicherlimits, ist davon nicht betroffen.

Ein Sandbox-Absturz führt dazu, dass alle Isolierungen in dieser Sandbox beendet werden. Die meisten offensichtliches Symptom hierfür ist, dass alle Bewertungen IsolateTerminatedException Je nach Situation bestimmte Ausnahmen wie SandboxDeadException oder MemoryLimitExceededException kann ausgegeben werden.

Der Umgang mit Abstürzen für jede einzelne Bewertung ist nicht immer praktikabel. Darüber hinaus kann ein Isolator außerhalb eines explizit angeforderten einer Bewertung aufgrund von Hintergrundaufgaben oder Bewertungen in anderen Isolierungen. Der Absturz kann die Verarbeitungslogik zentralisiert werden, indem ein Callback mithilfe von 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);

Optionale Sandbox-Funktionen

Abhängig von der zugrunde liegenden WebView-Version kann eine Sandbox-Implementierung verschiedene Funktionen zur Verfügung. Daher müssen alle erforderlichen Abfragen mithilfe von JavaScriptSandbox.isFeatureSupported(...). Es ist wichtig, um den Funktionsstatus zu überprüfen, bevor Methoden aufgerufen werden, die diese Funktionen nutzen.

JavaScriptIsolate-Methoden, die möglicherweise nicht überall verfügbar sind, die mit dem Vermerk RequiresFeature versehen sind, damit Sie sie leichter erkennen Anrufe im Code.

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. Wird die Funktion nicht unterstützt, werden alle Daten Die JavaScriptEngine erfolgt über eine Binder-Transaktion. 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 dem Binder Größenbeschränkung für Transaktionen, wenn JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT ist nicht unterstützt. Nicht-String-Werte müssen explizit in einen JavaScript-String konvertiert werden Andernfalls wird ein leerer String zurückgegeben. Wenn JS_FEATURE_PROMISE_RETURN -Funktion unterstützt wird, kann der JavaScript-Code alternativ ein Promise-Objekt zurückgeben. zu String wird.

Für die Übergabe großer Byte-Arrays an die JavaScriptIsolate-Instanz können die provideNamedData(...) API verwenden. Die Nutzung dieser API ist nicht an Binder-Transaktionslimits Jedes Byte-Array muss mit einem eindeutigen 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(...) ü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 = "(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 alles mit anderen teilen. Das folgende Snippet führt zu

Hi from AAA!5

und

Uncaught Reference Error: a is not defined

da die Instanz „jsTwo“ keine Objekte sichtbar hat, die in „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-Support

Um diese Jetpack-Bibliothek mit Kotlin-Coroutinen zu verwenden, fügen Sie kotlinx-coroutines-guava 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 von einem Koroutinebereich aus aufgerufen werden: 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.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 der isolierten Umgebung anfordern, können Sie ihre Konfiguration. Um die Konfiguration zu optimieren, übergeben Sie die IsolateStartupParameters in JavaScriptSandbox.createIsolate(...)

Derzeit können mit Parametern die maximale Heap-Größe und die maximale Größe angegeben werden für Bewertungsrückgabewerte und -fehler.