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-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());
           }
       },
       mainThreadExecutor);

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 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 = "(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.getMessage()), 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
            }
        },
        mainExecutor
    )
}

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.