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 di JavaScript non interattivo, l'utilizzo della libreria JavaScriptEngine presenta i seguenti vantaggi:
Minore consumo di risorse, in quanto non è necessario allocare un'istanza WebView.
Può essere eseguita in un servizio (attività WorkManager).
Più ambienti isolati con overhead ridotto, consentendo all'applicazione eseguire contemporaneamente più snippet JavaScript.
Possibilità di trasmettere grandi quantità di dati utilizzando 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 è abbastanza costosa. È consentita una sola istanza JavaScriptSandbox
per applicazione. Un
IllegalStateException
viene generato quando un'applicazione tenta di allocare una
seconda istanza JavaScriptSandbox
. Tuttavia, se più ambienti di esecuzione
è possibile allocare diverse istanze JavaScriptIsolate
.
Quando non è più utilizzata, chiudi l'istanza della sandbox per liberare risorse. L'istanza JavaScriptSandbox
implementa un'interfaccia AutoCloseable
, che consente l'utilizzo di try-with-resources per semplici casi d'uso di blocco.
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. Possono essere allocati quando necessario, fornendo confini di sicurezza deboli per script di origini diverse o attivando l'esecuzione di JavaScript concorrente poiché JavaScript è per sua natura a thread singolo. Le chiamate successive alla stessa istanza condividono lo stesso stato, quindi è possibile creare alcuni dati in un primo momento ed elaborarli in un secondo momento nella stessa istanza di JavaScriptIsolate
.
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
Rilascia JavaScriptIsolate
esplicitamente chiamando il relativo metodo close()
.
La chiusura di un'istanza isolata che esegue codice JavaScript
(con un Future
incompleto) genera un IsolateTerminatedException
. L'isolamento viene successivamente ripulito in background se l'implementazione supporta JS_FEATURE_ISOLATE_TERMINATION
, come descritto nella sezione Gestire gli arresti anomali della sandbox di questa pagina. 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 e accedere a un'istanza JavaScriptIsolate
da 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 passato come String
e il risultato viene restituito 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 nelle directory dell'app.
La libreria supporta anche il logging della console, che può essere utilizzato per scopi di debugging. Questa impostazione può essere configurata utilizzando setConsoleCallback()
.
Poiché il contesto è persistente, puoi caricare il codice ed eseguirlo più volte
durante la vita del 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 e 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 JavaScript rileva un errore, viene creato un JavaScriptException
. Fai riferimento alle relative sottoclassi
per eccezioni più specifiche.
Gestione degli arresti anomali della sandbox
Tutto il codice JavaScript viene eseguito in un processo sandbox separato 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 sintomo più evidente è che tutte le valutazioni inizieranno a non riuscire con IsolateTerminatedException
. A seconda delle circostanze, potrebbero essere lanciate eccezioni più specifiche come SandboxDeadException
o MemoryLimitExceededException
.
Gestire gli arresti anomali per ogni singola valutazione non è sempre pratico.
Inoltre, un isolamento potrebbe terminare al di fuori di una valutazione esplicitamente richiesta a causa di attività in background o valutazioni in altri istanze. La logica di gestione degli arresti anomali può essere centralizzata collegando un callback utilizzando 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 della sandbox potrebbe avere diversi insiemi di funzionalità disponibili. Pertanto, è necessario eseguire query su ogni funzionalità obbligatoria 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.
Trasmissione dei parametri
Se JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
è supportato, le richieste di valutazione inviate al motore JavaScript non sono vincolate dai limiti di transazione del 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 restaurata come stringa ed è soggetta al limite di dimensione massima della transazione Binder se JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
non è supportato. I valori non di stringa devono essere convertiti esplicitamente in una stringa JavaScript
altrimenti viene restituita una stringa vuota. Se la funzionalità JS_FEATURE_PROMISE_RETURN
è supportata, il codice JavaScript può restituire in alternativa una promessa che si risolve in un String
.
Per passare array di byte di grandi dimensioni all'istanza JavaScriptIsolate
, puoi utilizzare l'API provideNamedData(...)
. L'utilizzo di questa API non è vincolato
dai limiti di transazioni di 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 passato utilizzando l'API provideNamedData(...)
, quindi compilato ed eseguito nel solito modo, come dimostrato 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 di JavaScriptIsolate
Tutte le istanze JavaScriptIsolate
sono indipendenti l'una dall'altra e non condividono nulla. 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);
Supporto di Kotlin
Per utilizzare questa libreria Jetpack con le coroutine Kotlin, aggiungi una dipendenza a
kotlinx-coroutines-guava
. In questo modo è possibile l'integrazione con
ListenableFuture
.
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}
Ora le API della libreria Jetpack possono essere chiamate da un ambito di coroutine, come dimostrato 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 ambiente isolato, puoi modificarne la configurazione. Per modificare la configurazione, passa l'istanza IsolateStartupParameters a JavaScriptSandbox.createIsolate(...)
.
Attualmente i parametri consentono di specificare le dimensioni massime dell'heap e le dimensioni massime per i valori restituiti e gli errori di valutazione.