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 prova con le risorse 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.