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.