Wykonywanie JavaScriptu i WebAssembly

Ocena JavaScript

Biblioteka Jetpack JavaScriptEngine umożliwia aplikacji oceniać kod JavaScript bez konieczności tworzenia instancji WebView.

W przypadku aplikacji wymagających nieinteraktywnej oceny JavaScriptu za pomocą funkcji Biblioteka JavaScriptEngine ma te zalety:

  • Niższe zużycie zasobów, ponieważ nie trzeba przydzielić WebView instancji.

  • Można to zrobić w usłudze (zadanie WorkManager).

  • Wiele izolowanych środowisk z niskim nakładem pracy, dzięki czemu aplikacja może uruchamianie kilku fragmentów kodu JavaScript jednocześnie.

  • Możliwość przekazywania dużych ilości danych przy użyciu wywołania interfejsu API.

Podstawowe wykorzystanie

Zacznij od utworzenia instancji JavaScriptSandbox. To daje z silnikiem JavaScriptu poza procesem.

ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
               JavaScriptSandbox.createConnectedInstanceAsync(context);

Zalecamy dostosowanie cyklu życia piaskownicy do cyklu życia który wymaga oceny JavaScriptu.

Na przykład komponent hostujący piaskownicę może być Activity lub Service Do hermetyzacji oceny JavaScript może być używany pojedynczy element Service dla wszystkich komponentów aplikacji.

Utrzymuj instancję JavaScriptSandbox, ponieważ jej alokacja jest dość dobra drogie. Dozwolona jest tylko 1 instancja JavaScriptSandbox na aplikację. An Interfejs IllegalStateException jest zgłaszany, gdy aplikacja próbuje przydzielić do drugiej instancji JavaScriptSandbox. Jeśli jednak wiele środowisk wykonawczych są wymagane, można przydzielić kilka instancji JavaScriptIsolate.

Gdy instancja piaskownicy nie jest już używana, zamknij ją, aby zwolnić zasoby. instancji JavaScriptSandbox implementuje interfejs AutoCloseable, który pozwala na korzystanie z funkcji try-with-resources w przypadku prostych przypadków użycia blokujących. Możesz też sprawdzić, czy cyklem życia instancji JavaScriptSandbox jest zarządzany przez komponent hostujący, zamykając go w wywołaniu zwrotnym onStop() dla aktywności lub w okresie onDestroy() za Usługę:

jsSandbox.close();

Instancja JavaScriptIsolate reprezentuje kontekst wykonywania kod JavaScript. Mogą być przydzielane w razie potrzeby, zapewniając słabe zabezpieczenia granic dla skryptów o różnym pochodzeniu lub włączania równoczesnego JavaScriptu ponieważ JavaScript jest z natury jednowątkowy. Kolejne wywołania do ta sama instancja ma ten sam stan, dlatego można utworzyć pewne dane a potem przetwórz je później w tej samej instancji JavaScriptIsolate.

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

Wyraźnie zwolnij JavaScriptIsolate, wywołując jego metodę close(). Zamykanie izolowanej instancji z kodem JavaScript (niekompletna wartość Future) daje IsolateTerminatedException. izolowane są później czyszczone w tle, jeśli implementacja obsługuje JS_FEATURE_ISOLATE_TERMINATION, jak opisano w obsługę awarii w piaskownicy. stronę. W przeciwnym razie czyszczenie zostanie przełożone do czasu, gdy wszystkie oczekujące oceny zostaną lub piaskownica jest zamknięta.

Aplikacja może utworzyć instancję JavaScriptIsolate i uzyskać do niej dostęp z poziomu w dowolnym wątku.

Teraz aplikacja jest gotowa do wykonania kodu 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);

Ten sam fragment kodu JavaScript dobrze sformatowany:

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

Fragment kodu jest przekazywany jako String, a wynik jest dostarczany w formacie String. Pamiętaj, że wywołanie evaluateJavaScriptAsync() zwraca ocenę wyniku ostatniego wyrażenia w kodzie JavaScript. Musi to być typu String JavaScript; w przeciwnym razie zwraca pustą wartość. Kod JavaScript nie powinien zawierać słowa kluczowego return. Jeśli piaskownica obsługuje określone funkcje, dodatkowe typy zwrotów (np. Promise String) może być możliwe.

Biblioteka obsługuje również ocenę skryptów, które mają postać AssetFileDescriptor lub ParcelFileDescriptor. Zobacz evaluateJavaScriptAsync(AssetFileDescriptor) i evaluateJavaScriptAsync(ParcelFileDescriptor), aby uzyskać więcej informacji. Te interfejsy API lepiej nadają się do oceny z pliku na dysku lub w aplikacji i katalogów.

Biblioteka obsługuje również logowanie konsoli, które mogą być używane do debugowania w celach informacyjnych. Możesz to skonfigurować za pomocą narzędzia setConsoleCallback().

Ponieważ kontekst się nie powtarza, możesz przesłać kod i wykonać go kilka razy w ciągu 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);

Oczywiście zmienne też są trwałe, więc można kontynuować fragment z:

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

Na przykład pełny fragment kodu przydzielający wszystkie niezbędne obiekty wykonanie kodu JavaScript może wyglądać tak:

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

Zalecamy użycie metody try-with-resources, aby upewnić się, że wszystkie przydzielone zasoby zasoby są zwolnione i nie są już używane. Zamykanie wyników piaskownicy we wszystkich oczekujących ocenach we wszystkich instancjach JavaScriptIsolate, które zakończyły się niepowodzeniem dzięki SandboxDeadException. Gdy ocena JavaScript napotyka wystąpi błąd, zostanie utworzony JavaScriptException. Odwoływanie się do jej podklas o bardziej szczegółowe wyjątki.

Postępowanie w przypadku awarii piaskownicy

Cały JavaScript jest wykonywany w osobnym procesie w piaskownicy poza Twoją głównej procedury zgłoszenia. Jeśli kod JavaScript powoduje ten proces w trybie piaskownicy może ulec awarii, np. z powodu wyczerpania limitu pamięci, nie wpłynie na ten proces.

Awaria piaskownicy spowoduje zamknięcie wszystkich izolacji w tej piaskownicy. Najbardziej Oczywistym objawem jest to, że wszystkie oceny zaczną się niepowodzeniem od IsolateTerminatedException W zależności od sytuacji bardziej określonych wyjątków, takich jak SandboxDeadException lub MemoryLimitExceededException może zostać rzucony.

Postępowanie w przypadku awarii przy każdej indywidualnej ocenie nie zawsze jest praktyczne. Ponadto izolacja może zakończyć się poza dozwolonym adresem oceny z powodu zadań w tle lub ocen w innych izolacjach. Wypadek logikę obsługi można scentralizować, dołączając wywołanie zwrotne za pomocą funkcji 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);

Opcjonalne funkcje piaskownicy

W zależności od bazowej wersji komponentu WebView implementacja piaskownicy może mieć różne zestawy funkcji. Należy więc sprawdzać wszystkie wymagane za pomocą JavaScriptSandbox.isFeatureSupported(...). To ważne w celu sprawdzenia stanu funkcji przed jej użyciem.

Metody JavaScriptIsolate, które mogą nie być dostępne wszędzie, są opatrzone adnotacjami RequiresFeature, które ułatwiają ich rozpoznanie, w celu uzyskania numeru.

Zaliczone parametry

Jeśli JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT to obsługiwane, żądania oceny wysyłane do mechanizmu JavaScript nie są powiązane przez limity transakcji powiązanych. Jeśli funkcja nie jest obsługiwana, wszystkie dane do JavaScript Engine jest realizowany w ramach transakcji Binder. Ogólne limitu rozmiaru transakcji ma zastosowanie do każdego wywołania, które przekazuje dane lub zwraca dane.

Odpowiedź jest zawsze zwracana jako ciąg znaków i podlega funkcji Binder maksymalny rozmiar transakcji, jeśli JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT to nie obsługiwane. Wartości inne niż ciągi muszą zostać jawnie przekonwertowane na ciąg znaków JavaScript w przeciwnym razie zwracany jest pusty ciąg. Jeśli JS_FEATURE_PROMISE_RETURN jest obsługiwana, kod JavaScript może alternatywnie zwrócić obietnicę do String.

W przypadku przekazywania tablic o dużych bajtach do instancji JavaScriptIsolate: może używać interfejsu API provideNamedData(...). Użycie tego interfejsu API nie jest związane z limity transakcji Binder. Każda tablica bajtów musi być przekazywana za pomocą unikalnego identyfikatora identyfikatora, którego nie można ponownie użyć.

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

Działający kod Wasm

Kod WebAssembly (Wasm) można przekazywać za pomocą funkcji provideNamedData(...) API, a potem skompiluj i wykonaj w zwykły sposób, jak pokazano poniżej.

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

Separacja odseparowania JavaScriptu

Wszystkie instancje JavaScriptIsolate są od siebie niezależne i nie udostępniać czegokolwiek. Ten fragment kodu daje wynik

Hi from AAA!5

i

Uncaught Reference Error: a is not defined

ponieważ instancja „jsTwo” nie ma widoczności obiektów utworzonych w „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);

Pomoc Kotlin

Aby używać tej biblioteki Jetpack z współrzędnymi Kotlin, dodaj zależność do kotlinx-coroutines-guava Umożliwia to integrację z ListenableFuture

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}

Interfejsy API biblioteki Jetpack można teraz wywoływać z zakresu współprogramu, poniżej:

// 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
    )
}

Parametry konfiguracji

W przypadku żądania instancji izolowanego środowiska możesz dostosować jej konfiguracji. Aby dostosować konfigurację, przekaż IsolateStartupParameters, aby JavaScriptSandbox.createIsolate(...).

Obecnie parametry pozwalają określić maksymalny i maksymalny rozmiar stosu zwraca wartości i błędy.