Cómo ejecutar JavaScript y WebAssembly

Evaluación de JavaScript

La biblioteca de Jetpack JavaScriptEngine permite que una aplicación evaluar el código JavaScript sin crear una instancia de WebView.

Para las aplicaciones que requieren una evaluación no interactiva de JavaScript, con el La biblioteca JavaScriptEngine tiene las siguientes ventajas:

  • Menor consumo de recursos, ya que no es necesario asignar un componente WebView instancia.

  • Se puede hacer en un Service (tarea de WorkManager).

  • Varios entornos aislados con baja sobrecarga, lo que permite que la aplicación ejecutar varios fragmentos de JavaScript al mismo tiempo.

  • Capacidad de pasar grandes cantidades de datos mediante una llamada a la API

Uso básico

Para comenzar, crea una instancia de JavaScriptSandbox. Esto representa un con el motor de JavaScript fuera del proceso.

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

Se recomienda alinear el ciclo de vida de la zona de pruebas con el ciclo de vida de la que necesita evaluación de JavaScript.

Por ejemplo, un componente que aloja la zona de pruebas puede ser Activity o Service Se podría usar un solo Service para encapsular la evaluación de JavaScript para todos los componentes de la aplicación.

Mantener la instancia JavaScriptSandbox, ya que su asignación es bastante costoso. Solo se permite una instancia de JavaScriptSandbox por aplicación. Los Se arroja IllegalStateException cuando una aplicación intenta asignar un segunda instancia de JavaScriptSandbox. Sin embargo, si hay varios entornos de ejecución se requieren, se pueden asignar varias instancias de JavaScriptIsolate.

Cuando ya no se use, cierra la instancia de la zona de pruebas para liberar recursos. El La instancia JavaScriptSandbox implementa una interfaz AutoCloseable, que permite usar recursos de prueba para casos de uso de bloqueo simples. Como alternativa, asegúrate de que el ciclo de vida de la instancia de JavaScriptSandbox esté administrado por el componente de hosting y cerrarlo en la devolución de llamada onStop() de una actividad. durante onDestroy() para un servicio:

jsSandbox.close();

Una instancia de JavaScriptIsolate representa un contexto para ejecutar Código JavaScript. Pueden asignarse cuando sea necesario, lo que proporciona una seguridad débil límites para secuencias de comandos de diferente origen o habilitación de JavaScript simultáneo ejecución, ya que JavaScript es de un solo subproceso por naturaleza. Las llamadas posteriores a la misma instancia comparten el mismo estado, por lo que es posible crear algunos datos primero y, luego, procesarlos más tarde en la misma instancia de JavaScriptIsolate.

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

Libera JavaScriptIsolate de forma explícita llamando a su método close(). Cerrar una instancia aislada que ejecuta código JavaScript (tener un Future incompleto) da como resultado un IsolateTerminatedException. El el aislamiento se limpia posteriormente en segundo plano si la implementación admite JS_FEATURE_ISOLATE_TERMINATION, como se describe en manejo de fallas en la zona de pruebas más adelante. . De lo contrario, la limpieza se pospone hasta que todas las evaluaciones pendientes completar o cerrar la zona de pruebas.

Una aplicación puede crear una instancia de JavaScriptIsolate y acceder a ella desde cualquier conversación.

Ahora, la aplicación está lista para ejecutar un código 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);

El mismo fragmento de JavaScript tiene el siguiente formato:

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

El fragmento de código se pasa como un String y el resultado se entrega como un String. Ten en cuenta que llamar a evaluateJavaScriptAsync() devuelve el valor resultado de la última expresión en el código JavaScript. Debe ser de tipo String de JavaScript; De lo contrario, la API de la biblioteca muestra un valor vacío. El código JavaScript no debe usar una palabra clave return. Si la zona de pruebas Admite determinadas funciones, tipos de datos que se muestran adicionales (por ejemplo, un Promise que se resuelve en un String).

La biblioteca también admite la evaluación de secuencias de comandos con la forma de una AssetFileDescriptor o ParcelFileDescriptor. Consulta evaluateJavaScriptAsync(AssetFileDescriptor) y evaluateJavaScriptAsync(ParcelFileDescriptor) para obtener más información. Estas APIs son más adecuadas para realizar evaluaciones desde un archivo en el disco o en la app. directorios.

La biblioteca también admite el registro de la consola, que se puede usar para la depuración. comerciales. Esto se puede configurar con setConsoleCallback().

Dado que el contexto persiste, puedes subir el código y ejecutarlo varias veces. durante la vida útil 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);

Por supuesto, las variables también son persistentes, así que puedes continuar con el fragmento 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);

Por ejemplo, el fragmento completo para asignar todos los objetos necesarios y la ejecución de un código JavaScript podría verse de la siguiente manera:

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

Recomendamos que uses la prueba con recursos para asegurarte de que todos los recursos recursos se liberan y ya no se usan. Cómo cerrar los resultados de la zona de pruebas en todas las evaluaciones pendientes en las JavaScriptIsolate instancias con errores con un SandboxDeadException. Cuando se encuentra la evaluación de JavaScript se produce un error, se crea una JavaScriptException. Haz referencia a sus subclases para excepciones más específicas.

Controla las fallas de la zona de pruebas

Todo JavaScript se ejecuta en un proceso de zona de pruebas independiente, lejos de tu proceso principal de la aplicación. Si el código JavaScript provoca este proceso de zona de pruebas falle, por ejemplo, por agotar un límite de memoria, la configuración principal proceso no se verá afectado.

Una falla de la zona de pruebas hará que finalicen todos los elementos aislados de esa zona de pruebas. El más un síntoma obvio de esto es que todas las evaluaciones comenzarán a fallar con IsolateTerminatedException Según las circunstancias, más excepciones específicas, como SandboxDeadException o Es posible que se arroje MemoryLimitExceededException.

El manejo de fallas para cada evaluación individual no siempre es práctico. Además, un aislamiento puede finalizar fuera de una solicitud explícita debido a tareas en segundo plano o evaluaciones en otros aislamientos. El accidente automovilístico de administración de identidades y administrar la lógica central se puede centralizar adjuntando una devolución de llamada mediante 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);

Funciones opcionales de la zona de pruebas

Según la versión subyacente de WebView, es posible que una implementación de zona de pruebas tenga diferentes conjuntos de funciones disponibles. Por eso, es necesario consultar cada uno con JavaScriptSandbox.isFeatureSupported(...). Es importante para verificar el estado de las funciones antes de llamar a los métodos que dependen de ellas.

Los métodos JavaScriptIsolate que podrían no estar disponibles en todas partes son con la anotación RequiresFeature, lo que facilita la detección llamadas en el código.

Cómo pasar parámetros

Si JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT es compatible, las solicitudes de evaluación enviadas al motor de JavaScript no están vinculadas por los límites de transacciones de Binder. Si la función no es compatible, todos los datos a JavaScriptEngine se produce mediante una transacción de Binder. El grupo general el límite de tamaño de las transacciones es aplicable a todas las llamadas que pasan datos o devuelve datos.

La respuesta siempre se muestra como una String y está sujeta a Binder el límite de tamaño máximo de la transacción JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT no es no es compatible. Los valores que no sean de cadena deben convertirse explícitamente en una cadena de JavaScript De lo contrario, se muestra una cadena vacía. Si es JS_FEATURE_PROMISE_RETURN se admite esta función, el código JavaScript puede, como alternativa, mostrar una promesa que se resuelve en una String.

Para pasar arrays de bytes grandes a la instancia JavaScriptIsolate, debes puedes usar la API de provideNamedData(...). El uso de esta API no está vinculado por los límites de transacciones de Binder. Cada array de bytes se debe pasar usando una regla identificador único que no se puede volver a usar.

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

Ejecutando código de Wasm

El código WebAssembly (Wasm) se puede pasar con provideNamedData(...). después se compila y ejecuta de la manera habitual, como se muestra a continuación.

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

Separación de JavaScriptIsolate

Todas las instancias de JavaScriptIsolate son independientes entre sí y no compartir nada. El siguiente fragmento da como resultado

Hi from AAA!5

y

Uncaught Reference Error: a is not defined

porque la instancia “jsTwo” no tiene visibilidad de los objetos que se crean en "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);

Compatibilidad con Kotlin

Para usar esta biblioteca de Jetpack con corrutinas de Kotlin, agrega una dependencia a kotlinx-coroutines-guava Esto permite la integración ListenableFuture

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

Ahora se puede llamar a las APIs de la biblioteca de Jetpack desde un alcance de corrutinas, como se muestra a continuación:

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

Parámetros de configuración

Al solicitar una instancia de entorno aislado, puedes modificar su configuración. Para ajustar la configuración, pasa el IsolateStartupParameters para JavaScriptSandbox.createIsolate(...)

Actualmente, los parámetros permiten especificar el tamaño máximo de montón y el tamaño máximo. para la evaluación, devolver valores y errores.