Esecuzione di JavaScript e WebAssembly

Valutazione JavaScript

La libreria Jetpack, JavaScriptEngine, consente a un'applicazione di Valutare il codice JavaScript senza creare un'istanza WebView.

Per le applicazioni che richiedono la valutazione JavaScript non interattiva, puoi utilizzare La libreria JavaScriptEngine presenta i seguenti vantaggi:

  • Consumo di risorse inferiore, poiché non è necessario allocare un componente WebView in esecuzione in un'istanza Compute Engine.

  • Può essere eseguita in un servizio (attività WorkManager).

  • Più ambienti isolati con overhead ridotto, consentendo all'applicazione eseguire contemporaneamente più snippet JavaScript.

  • Capacità di passare grandi quantità di dati mediante una chiamata API.

Utilizzo di base

Per iniziare, crea un'istanza di JavaScriptSandbox. Questo rappresenta connessione al motore JavaScript out-of-process.

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

È consigliabile allineare il ciclo di vita della sandbox a quello della che richiede la valutazione JavaScript.

Ad esempio, un componente che ospita la sandbox può essere un Activity o un Service. È possibile utilizzare un singolo Service per incapsulare la valutazione JavaScript per tutti i componenti dell'applicazione.

Mantieni l'istanza JavaScriptSandbox perché la sua allocazione è discreta costoso. È consentita una sola istanza JavaScriptSandbox per applicazione. Un Viene restituito IllegalStateException quando un'applicazione tenta di allocare seconda istanza JavaScriptSandbox. Tuttavia, se più ambienti di esecuzione è possibile allocare diverse istanze JavaScriptIsolate.

Quando non viene più utilizzata, chiudi l'istanza sandbox per liberare risorse. La L'istanza JavaScriptSandbox implementa un'interfaccia AutoCloseable, che consente l'uso di tipo "try-with-resources" per casi d'uso di blocco semplici. In alternativa, assicurati che il ciclo di vita dell'istanza JavaScriptSandbox sia gestito da il componente hosting, chiudendolo nel callback onStop() per un'attività o durante il giorno onDestroy() per un servizio:

jsSandbox.close();

Un'istanza JavaScriptIsolate rappresenta un contesto per l'esecuzione codice JavaScript. Quando necessario, possono essere allocati, fornendo una sicurezza debole limiti per script di origini diverse o abilitando JavaScript in contemporanea poiché JavaScript è a thread unico per natura. Chiamate successive a la stessa istanza condivide lo stesso stato, quindi è possibile creare alcuni dati per poi elaborarlo in un secondo momento nella stessa istanza di JavaScriptIsolate.

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

Rilascia JavaScriptIsolate in modo esplicito chiamando il relativo metodo close(). Chiusura di un'istanza isolata che esegue codice JavaScript in corso... (se il file Future è incompleto) genera un IsolateTerminatedException. La viene ripulito successivamente in background se supporta JS_FEATURE_ISOLATE_TERMINATION, come descritto sezione sulla gestione degli arresti anomali della sandbox più avanti. . In caso contrario, la pulizia viene posticipata fino a quando tutte le valutazioni in attesa viene completata o la sandbox viene chiusa.

Un'applicazione può creare un'istanza JavaScriptIsolate e accedervi da in qualsiasi thread.

Ora l'applicazione è pronta per eseguire del codice 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);

Lo stesso snippet JavaScript formattato correttamente:

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

Lo snippet di codice viene trasmesso come String e il risultato viene pubblicato come String. Tieni presente che la chiamata a evaluateJavaScriptAsync() restituisce i risultati risultato dell'ultima espressione nel codice JavaScript. Deve essere di tipo JavaScript String; altrimenti l'API della libreria restituisce un valore vuoto. Il codice JavaScript non deve utilizzare una parola chiave return. Se la sandbox supporta alcune funzionalità, tipi di reso aggiuntivi (ad esempio, un Promise che si risolve in un String).

La libreria supporta anche la valutazione di script sotto forma di AssetFileDescriptor o ParcelFileDescriptor. Consulta evaluateJavaScriptAsync(AssetFileDescriptor) e evaluateJavaScriptAsync(ParcelFileDescriptor) per ulteriori dettagli. Queste API sono più adatte per la valutazione da un file su disco o in-app .

La libreria supporta anche il logging della console, che può essere utilizzato per il debug scopi. Questa opzione può essere configurata utilizzando setConsoleCallback().

Poiché il contesto persiste, puoi caricare il codice ed eseguirlo più volte per tutta la durata di 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);

Ovviamente, anche le variabili sono permanenti, quindi puoi continuare con snippet con:

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

Ad esempio, lo snippet completo per l'allocazione di tutti gli oggetti necessari l'esecuzione di un codice JavaScript potrebbe avere il seguente aspetto:

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

Ti consigliamo di usare test-with-resources per assicurarti che tutte le risorse vengono rilasciate e non vengono più utilizzate. Chiusura dei risultati della sandbox in tutte le valutazioni in attesa e in tutte le JavaScriptIsolate istanze con errori con un SandboxDeadException. Quando la valutazione di JavaScript rileva un errore, viene creata una JavaScriptException. Fai riferimento alle relative sottoclassi per eccezioni più specifiche.

Gestione degli arresti anomali della sandbox

Tutto JavaScript viene eseguito in un processo sandbox separato, lontano dal processo principale dell'applicazione. Se il codice JavaScript causa questo processo sandbox si arresta in modo anomalo, ad esempio esaurendo un limite di memoria, e il processo non sarà interessato.

Un arresto anomalo della sandbox causerà la terminazione di tutti gli isolati in quella sandbox. Il più il sintomo ovvio è che tutte le valutazioni inizieranno a non riuscire IsolateTerminatedException A seconda dei casi, eccezioni specifiche come SandboxDeadException MemoryLimitExceededException potrebbe essere lanciato.

La gestione degli arresti anomali per ogni singola valutazione non è sempre pratica. Inoltre, un isolato può terminare al di fuori di una richiesta esplicita la valutazione grazie ad attività in background o valutazioni in altri isolati. L'incidente la logica di gestione può essere centralizzata collegando un callback 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);

Funzionalità facoltative della sandbox

A seconda della versione di WebView sottostante, un'implementazione sandbox potrebbe avere diversi insiemi di funzionalità disponibili. Quindi, è necessario eseguire query su ogni utilizzando JavaScriptSandbox.isFeatureSupported(...). È importante per controllare lo stato delle caratteristiche prima di chiamare metodi basati su queste caratteristiche.

I metodi di JavaScriptIsolate che potrebbero non essere disponibili ovunque sono annotati con l'annotazione RequiresFeature, che ne facilitano l'individuazione chiamate nel codice.

Parametri per il passaggio

Se JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT è supportato, le richieste di valutazione inviate al motore JavaScript non sono associate in base ai limiti delle transazioni di binder. Se la funzione non è supportata, tutti i dati per il motore JavaScript avviene tramite una transazione Binder. Il generale limite per le dimensioni delle transazioni si applica a ogni chiamata trasmessa nei dati o restituisce i dati.

La risposta viene sempre restituita come stringa ed è soggetta al Binder la dimensione massima della transazione se JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT non è supportati. I valori non stringa devono essere convertiti esplicitamente in stringa JavaScript altrimenti viene restituita una stringa vuota. Se JS_FEATURE_PROMISE_RETURN è supportata, il codice JavaScript può in alternativa restituire una promessa risoluzione con String.

Per passare array di byte di grandi dimensioni all'istanza JavaScriptIsolate, può utilizzare l'API provideNamedData(...). L'utilizzo di questa API non è vincolato da limiti di transazione a Binder. Ogni array di byte deve essere passato utilizzando un indirizzo identificatore che non può essere riutilizzato.

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

Esecuzione del codice Wasm

Il codice WebAssembly (Wasm) può essere trasmesso utilizzando provideNamedData(...) dell'API, quindi compilata ed eseguita secondo le modalità consuete, come illustrato di seguito.

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

Separazione isola JavaScript

Tutte le istanze JavaScriptIsolate sono indipendenti l'una dall'altra e non condividere qualsiasi cosa. Il seguente snippet restituisce

Hi from AAA!5

e

Uncaught Reference Error: a is not defined

poiché l'istanza "jsTwo" non ha visibilità degli oggetti creati 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);

Assistenza Kotlin

Per usare questa libreria Jetpack con le coroutine Kotlin, aggiungi una dipendenza a kotlinx-coroutines-guava Ciò consente l'integrazione ListenableFuture.

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

Ora le API della libreria Jetpack possono essere chiamate da un ambito coroutine, come mostrato di seguito:

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

Parametri di configurazione

Quando richiedi un'istanza di un ambiente isolato, puoi modificarne configurazione. Per modificare la configurazione, passa il IsolateStartupParameters su JavaScriptSandbox.createIsolate(...)

I parametri attualmente consentono di specificare la dimensione massima dell'heap e quella massima per la valutazione restituiscono valori ed errori.