Exécuter JavaScript et WebAssembly

É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.

  • Cette opération peut être effectuée dans un service (tâche WorkManager).

  • Environnements isolés à faible coût, permettant à l'application de exécuter plusieurs extraits de code 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 élément 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 est autorisée par application. 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 plusieurs instances JavaScriptIsolate peuvent être allouées.

Lorsqu'elle n'est plus utilisée, 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 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. Appels ultérieurs vers une même instance partagent le même état, il est donc possible de créer des données puis de le 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 isolée exécutant du code JavaScript (un Future incomplet) génère une IsolateTerminatedException. La sont nettoyés par la suite en arrière-plan si l'implémentation est compatible avec JS_FEATURE_ISOLATE_TERMINATION, comme décrit dans les gestion des plantages du bac à sable plus loin dans cet article. . 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 une instance JavaScriptIsolate et y accéder depuis n'importe quel fil de discussion.

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

Le même extrait de code JavaScript est correctement formaté:

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 envoyé 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 sous la forme d'un 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 une application répertoires.

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 entendu, les variables sont également persistantes, ce qui vous permet de poursuivre extrait 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 complet permettant d'allouer tous les objets nécessaires l'exécution d'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 ressources 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. Lorsque l'évaluation JavaScript rencontre une erreur, une JavaScriptException est créée. Reportez-vous à ses sous-classes pour 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 Selon les circonstances, plus des exceptions spécifiques, telles que SandboxDeadException ou L'erreur MemoryLimitExceededException risque d'être générée.

Gérer les plantages pour chaque évaluation n'est pas toujours pratique. De plus, un élément isolé peut prendre fin en dehors d'une l'évaluation en raison de tâches d'arrière-plan ou d'évaluations dans d'autres 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 de bac à sable facultatives

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 ressource à 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 les requêtes d'évaluation envoyées au moteur JavaScript ne sont pas liées par les limites de transaction de liaison. Si la fonctionnalité n'est pas disponible, toutes les données à JavaScriptEngine s'effectue via une transaction de liaison. Le général limite de taille de transaction s'applique à chaque appel qui transmet des données 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 qui ne sont pas des chaînes doivent être explicitement converties en chaîne JavaScript sinon une chaîne vide est renvoyée. Si JS_FEATURE_PROMISE_RETURN est prise en charge, le code JavaScript peut également renvoyer une promesse à la résolution dans un String.

Pour transmettre des tableaux d'octets volumineux à l'instance JavaScriptIsolate, vous devez : peuvent utiliser l'API provideNamedData(...). L'utilisation de cette API n'est pas soumise aux les limites de transaction de liaison. Chaque tableau d'octets doit être transmis à l'aide d'un 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ée et exécutée de la manière habituelle, comme illustré 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é 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 et la taille maximale du tas de mémoire les valeurs renvoyées et les erreurs d'évaluation.