Wykonywanie JavaScriptu i WebAssembly

Ocena JavaScript

Biblioteka Jetpack JavaScriptEngine umożliwia aplikacji interpretowanie kodu JavaScript bez tworzenia wystąpienia WebView.

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

  • Mniejsze zużycie zasobów, ponieważ nie trzeba przydzielać instancji WebView.

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

  • Wiele izolowanych środowisk o małym narzucie, które umożliwiają aplikacji jednoczesne uruchamianie wielu fragmentów kodu JavaScript.

  • możliwość przesyłania dużych ilości danych za pomocą wywołania interfejsu API;

Podstawowe zastosowanie

Na początek utwórz instancję JavaScriptSandbox. To daje z silnikiem JavaScriptu poza procesem.

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

Zalecamy dostosowanie cyklu życia piaskownicy do cyklu życia komponentu, który wymaga oceny kodu JavaScript.

Na przykład komponent hostujący piaskownicę może być Activity lub Service. Pojedynczy element Service może służyć do otaczania oceny JavaScriptu we wszystkich komponentach aplikacji.

Zachowaj instancję JavaScriptSandbox, ponieważ jej przydzielenie jest dość 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 zasobów try-with-zasobów w prostych przypadkach użycia blokujących. Możesz też zarządzać cyklem życia instancji JavaScriptSandbox za pomocą komponentu hostującego, zamykając ją w funkcji wywołania zwrotnego onStop() w przypadku aktywności lub w funkcji onDestroy() w przypadku usługi:

jsSandbox.close();

Instancja JavaScriptIsolate reprezentuje kontekst wykonywania kod JavaScript. W razie potrzeby można je przydzielić, co zapewnia słabe zabezpieczenia dla skryptów o różnym pochodzeniu lub umożliwia równoległe wykonywanie kodu JavaScript, ponieważ JavaScript jest z założenia 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();

Wypuść JavaScriptIsolate w sposób jawny, wywołując metodę close(). Zamknięcie izolowanego wystąpienia z uruchomionym kodem JavaScript (z niekompletnym Future) powoduje IsolateTerminatedException. Isolate jest następnie oczyszczany w tle, jeśli implementacja obsługuje JS_FEATURE_ISOLATE_TERMINATION, zgodnie z opisem w sekcji obsługa błędów w piaskownicy na tej stronie. W przeciwnym razie czyszczenie zostanie odroczone do czasu zakończenia wszystkich oczekujących ocen lub zamknięcia piaskownicy.

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 zweryfikowany wynik ostatniego wyrażenia w kodzie JavaScript. Musi to być typ JavaScript String; w przeciwnym razie interfejs API biblioteki zwróci pustą wartość. Kod JavaScript nie powinien zawierać słowa kluczowego return. Jeśli piaskownica obsługuje określone funkcje, mogą być dostępne dodatkowe typy zwracanych danych (np. Promise, które przekształca się w String).

Biblioteka obsługuje również ocenę skryptów, które mają postać AssetFileDescriptor lub ParcelFileDescriptor. Więcej informacji znajdziesz w sekcji evaluateJavaScriptAsync(AssetFileDescriptor)evaluateJavaScriptAsync(ParcelFileDescriptor). 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);

Zmienne są oczywiście trwałe, więc możesz kontynuować poprzedni fragment kodu za pomocą:

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

Przykładowy fragment kodu służący do przydzielenia wszystkich niezbędnych obiektów i wykonania 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 polecenia try-with-resources, aby mieć pewność, że wszystkie przydzielone zasoby zostały 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 podczas oceny JavaScriptu wystąpi błąd, zostanie utworzony element JavaScriptException. Odwoływanie się do jej podklas o bardziej szczegółowe wyjątki.

Postępowanie w przypadku awarii w trybie 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 zakończenie wszystkich izolowanych procesów w tej piaskownicy. Najbardziej widocznym objawem jest to, że wszystkie oceny zaczną się kończyć niepowodzeniem z wartością IsolateTerminatedException. W zależności od okoliczności mogą wystąpić bardziej szczegółowe wyjątki, takie jak SandboxDeadException lub MemoryLimitExceededException.

Obsługa awarii w przypadku każdej oceny nie zawsze jest praktyczna. Ponadto izolację można zakończyć poza wyraźnie żądaną oceną z powodu zadań w tle lub ocen w innych izolatach. Logika obsługi awarii może być scentralizowana przez dołączenie wywołania zwrotnego 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 wersji WebView implementacja piaskownicy może mieć dostępne różne zestawy funkcji. Dlatego należy wysłać zapytanie do każdej wymaganej funkcji za pomocą JavaScriptSandbox.isFeatureSupported(...). To ważne w celu sprawdzenia stanu funkcji przed jej wywołaniem za pomocą metod tych funkcji.

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.

Przekazywanie parametrów

Jeśli obsługiwana jest funkcja JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT, żądania oceny wysyłane do silnika JavaScript nie są ograniczone przez limity transakcji bindera. Jeśli funkcja nie jest obsługiwana, wszystkie dane do JavaScriptEngine są przesyłane za pomocą transakcji Binder. Ogólny limit rozmiaru transakcji dotyczy każdego wywołania, które przekazuje 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 znaków muszą być jawnie konwertowane na ciąg znaków JavaScript, w przeciwnym razie zwracany jest pusty ciąg znaków. 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(...). Korzystanie z tego interfejsu API nie jest ograniczone limitami transakcji Binder. Każda tablica bajtów musi być przekazywana za pomocą unikalnego 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);
}

Uruchamianie kodu Wasm

Kod WebAssembly (Wasm) można przekazać za pomocą interfejsu API provideNamedData(...), a następnie skompilować i wykonywać 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 powoduje, że

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 Jetpacka z koroboidami Kotlina, 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 Jetpacka można teraz wywoływać w zakresie współbieżności, jak pokazano 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

Gdy żądasz instancji odizolowanego środowiska, możesz dostosować jego konfigurację. Aby dostosować konfigurację, prześlij instancję IsolateStartupParameters do JavaScriptSandbox.createIsolate(...).

Obecnie parametry umożliwiają określenie maksymalnego rozmiaru stosu i maksymalnego rozmiaru wartości zwracanych przez funkcję oceny i błędów.