Évaluation JavaScript
La bibliothèque Jetpack JavaScriptEngine permet à une application de évaluer le code JavaScript sans créer d'instance WebView.
Pour les applications nécessitant une évaluation JavaScript non interactive, utilisez la classe La bibliothèque JavaScriptEngine présente les avantages suivants:
Réduction de la consommation de ressources, car il n'est pas nécessaire d'allouer un composant WebView Compute Engine.
Peut être effectué dans un service (tâche WorkManager).
Plusieurs environnements isolés avec un coût supplémentaire faible, ce qui permet à l'application d'exécuter plusieurs extraits JavaScript simultanément.
Vous êtes capable de transmettre de grandes quantités de données à l'aide d'un appel d'API.
Utilisation de base
Pour commencer, créez une instance de JavaScriptSandbox
. Cela représente
au moteur JavaScript hors processus.
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
Il est recommandé d'aligner le cycle de vie du bac à sable sur celui du qui nécessite une évaluation JavaScript.
Par exemple, un composant hébergeant le bac à sable peut être un Activity
ou un Service
. Un seul Service
peut être utilisé pour encapsuler l'évaluation JavaScript pour tous les composants de l'application.
Conserver l'instance JavaScriptSandbox
, car son allocation est équitable
chers. Une seule instance JavaScriptSandbox
par application est autorisée. Une
L'exception IllegalStateException
est générée lorsqu'une application tente d'allouer une
une deuxième instance JavaScriptSandbox
. Toutefois, si plusieurs environnements d'exécution sont requis, plusieurs instances JavaScriptIsolate
peuvent être allouées.
Lorsque vous n'en avez plus besoin, fermez l'instance de bac à sable pour libérer des ressources. La
L'instance JavaScriptSandbox
implémente une interface AutoCloseable
, qui
permet d'utiliser des ressources try-with-resources pour des cas d'utilisation de blocage simples.
Vous pouvez également vous assurer que le cycle de vie de l'instance JavaScriptSandbox
est géré par
le composant d'hébergement, en le fermant dans le rappel onStop()
d'une activité ;
pendant onDestroy()
pour un service:
jsSandbox.close();
Une instance JavaScriptIsolate
représente un contexte d'exécution du code JavaScript. Elles peuvent être allouées en cas de besoin, ce qui offre une sécurité faible
des limites pour les scripts d'origines différentes ou activer simultanément des scripts JavaScript
puisque JavaScript est, par nature, monothread. Les appels ultérieurs à la même instance partagent le même état. Il est donc possible de créer d'abord des données, puis de les traiter plus tard dans la même instance de JavaScriptIsolate
.
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
Libérez JavaScriptIsolate
explicitement en appelant sa méthode close()
.
Fermer une instance d'isolateur exécutant du code JavaScript (avec un Future
incomplet) génère une IsolateTerminatedException
. L'isolate est ensuite nettoyé en arrière-plan si l'implémentation est compatible avec JS_FEATURE_ISOLATE_TERMINATION
, comme décrit dans la section Gérer les plantages du bac à sable plus loin sur cette page. Sinon, le nettoyage est reporté jusqu'à ce que toutes les évaluations en attente soient
terminé ou que le bac à sable est fermé.
Une application peut créer et accéder à une instance JavaScriptIsolate
à partir de n'importe quel thread.
L'application est maintenant prête à exécuter du code 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);
Même extrait de code JavaScript, mis en forme :
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);
L'extrait de code est transmis en tant que String
et le résultat est fourni en tant que String
.
Notez que l'appel de evaluateJavaScriptAsync()
renvoie l'état
résultat de la dernière expression dans le code JavaScript. Il doit s'agir
de type String
JavaScript ; Sinon, l'API de la bibliothèque renvoie une valeur vide.
Le code JavaScript ne doit pas utiliser de mot clé return
. Si le bac à sable
prend en charge certaines fonctionnalités ou des types renvoyés supplémentaires (par exemple, un Promise
qui renvoie vers une String
) est possible.
La bibliothèque prend également en charge l'évaluation des scripts au format AssetFileDescriptor
ou ParcelFileDescriptor
. Voir
evaluateJavaScriptAsync(AssetFileDescriptor)
et
evaluateJavaScriptAsync(ParcelFileDescriptor)
pour en savoir plus.
Ces API sont plus adaptées à l'évaluation à partir d'un fichier sur disque ou dans des répertoires d'application.
La bibliothèque est également compatible avec la journalisation de la console, qui peut être utilisée pour le débogage
objectifs. Vous pouvez configurer cela à l'aide de setConsoleCallback()
.
Comme le contexte persiste, vous pouvez importer du code et l'exécuter plusieurs fois
pendant la durée de vie de 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);
Bien sûr, les variables sont également persistantes. Vous pouvez donc continuer l'extrait précédent avec :
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);
Par exemple, l'extrait de code complet permettant d'allouer tous les objets nécessaires et d'exécuter un code JavaScript peut se présenter comme suit :
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);
Nous vous recommandons d'utiliser try-with-resources pour vous assurer que toutes les ressources allouées sont libérées et ne sont plus utilisées. Résultats de la fermeture du bac à sable
dans toutes les évaluations en attente, dans toutes les instances JavaScriptIsolate
en échec
avec un SandboxDeadException
. Lorsqu'une erreur se produit lors de l'évaluation JavaScript, un JavaScriptException
est créé. Reportez-vous à ses sous-classes pour obtenir des exceptions plus spécifiques.
Gérer les plantages de bac à sable
Tout le code JavaScript est exécuté dans un processus de bac à sable distinct, en dehors de votre le processus principal de l'application. Si le code JavaScript provoque ce processus en bac à sable peut planter, par exemple en épuisant une limite de mémoire, le processus ne sera pas affecté.
Un plantage du bac à sable entraîne l'arrêt de tous les éléments isolés de ce bac à sable. Les plus
le symptôme évident de cette tendance est que toutes les évaluations échoueront
IsolateTerminatedException
En fonction des circonstances, des exceptions plus spécifiques telles que SandboxDeadException
ou MemoryLimitExceededException
peuvent être générées.
Gérer les plantages pour chaque évaluation n'est pas toujours pratique.
De plus, un isolate peut se terminer en dehors d'une évaluation explicitement demandée en raison de tâches en arrière-plan ou d'évaluations dans d'autres composants isolés. L'accident
la logique de gestion peut être centralisée en associant un rappel à l'aide de
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);
Fonctionnalités facultatives du bac à sable
Selon la version WebView sous-jacente, une implémentation de bac à sable peut avoir
de fonctionnalités différentes. Il est donc nécessaire d'interroger chaque fonctionnalité requise à l'aide de JavaScriptSandbox.isFeatureSupported(...)
. Il est important
pour vérifier l'état des fonctionnalités avant d'appeler des méthodes qui s'appuient dessus.
Les méthodes JavaScriptIsolate
qui peuvent ne pas être disponibles partout sont
annotée avec RequiresFeature
, ce qui vous permet de les repérer plus facilement
dans le code.
Paramètres transmis
Si JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
est compatible, les requêtes d'évaluation envoyées au moteur JavaScript ne sont pas limitées par les limites de transaction du liaisonneur. Si la fonctionnalité n'est pas disponible, toutes les données à
JavaScriptEngine s'effectue via une transaction Binder. La limite générale de taille des transactions s'applique à chaque appel qui transmet ou renvoie des données.
La réponse est toujours renvoyée sous forme de chaîne et soumise à la liaison
limite de taille maximale de la transaction si
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
n'est pas
compatibles. Les valeurs autres que des chaînes doivent être converties explicitement en chaîne JavaScript, sinon une chaîne vide est renvoyée. Si la fonctionnalité JS_FEATURE_PROMISE_RETURN
est prise en charge, le code JavaScript peut également renvoyer une promesse se résolvant en String
.
Pour transmettre de grands tableaux d'octets à l'instance JavaScriptIsolate
, vous pouvez utiliser l'API provideNamedData(...)
. L'utilisation de cette API n'est pas limitée par les limites de transaction de Binder. Chaque tableau d'octets doit être transmis à l'aide d'un identifiant unique qui ne peut pas être réutilisé.
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);
}
Exécution du code Wasm
Le code WebAssembly (Wasm) peut être transmis à l'aide du provideNamedData(...)
de l'API, puis compilé et exécuté de la manière habituelle, comme indiqué ci-dessous.
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);
}
Séparation de JavaScriptIsolate
Toutes les instances JavaScriptIsolate
sont indépendantes les unes des autres et ne
partager
n'importe quoi. L'extrait de code suivant génère
Hi from AAA!5
et
Uncaught Reference Error: a is not defined
car l'instance "jsTwo
" n'a pas de visibilité sur les objets créés dans
"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);
Compatibilité avec Kotlin
Pour utiliser cette bibliothèque Jetpack avec des coroutines Kotlin, ajoutez une dépendance à
kotlinx-coroutines-guava
Cela permet l'intégration avec
ListenableFuture
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}
Les API de la bibliothèque Jetpack peuvent désormais être appelées à partir d'un champ d'application de coroutine, comme indiqué ci-dessous:
// 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
)
}
Paramètres de configuration
Lorsque vous demandez une instance d'environnement isolé, vous pouvez ajuster sa
configuration. Pour ajuster la configuration, transmettez la méthode
l'instance IsolateStartupParameters sur
JavaScriptSandbox.createIsolate(...)
Actuellement, les paramètres permettent de spécifier la taille maximale de la pile et la taille maximale pour les valeurs de retour et les erreurs d'évaluation.