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.
En el caso de las aplicaciones que requieren una evaluación de JavaScript no interactiva, usar 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 servicio (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 del componente que necesita la evaluación de JavaScript.
Por ejemplo, un componente que aloja la zona de pruebas puede ser Activity
o
Service
Se puede usar un solo Service
para encapsular la evaluación de JavaScript para todos los componentes de la aplicación.
Mantén la instancia de JavaScriptSandbox
porque su asignación es bastante costosa. Solo se permite una instancia de JavaScriptSandbox
por aplicación. Se arroja una IllegalStateException
cuando una aplicación intenta asignar una 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 zona de pruebas para liberar recursos. La instancia de JavaScriptSandbox
implementa una interfaz AutoCloseable
, que permite el uso de try-with-resources para casos de uso de bloqueo simples.
Como alternativa, asegúrate de que el ciclo de vida de la instancia de JavaScriptSandbox
sea 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 de aislamiento que ejecuta código JavaScript (con un Future
incompleto) genera un IsolateTerminatedException
. El aislamiento se limpia posteriormente en segundo plano si la implementación admite JS_FEATURE_ISOLATE_TERMINATION
, como se describe en la sección Cómo controlar las fallas de la zona de pruebas más adelante en esta página. De lo contrario, la limpieza se pospone hasta que se completen todas las evaluaciones pendientes o se cierre la zona de pruebas.
Una aplicación puede crear una instancia de JavaScriptIsolate
y acceder a ella desde cualquier subproceso.
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)";
Listen<ableFu>tureString 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()
muestra el resultado evaluado 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 que tienen el formato de un AssetFileDescriptor
o un 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()
.
Como el contexto persiste, puedes subir 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(); }";
Listen<ableFu>tureString func = js.evaluateJavaScriptAsync(jsFunction);
String twoPlusThreeCode = "let five = sum(2, 3); five&quo<t;;
Li>stenableFutureString r1 = Futures.transformAsync(>func,
input - js.evaluateJavaScriptAsync(twoPlusThreeCode)
, executor);
String twoPlusThree = r1.get(5, TimeUnit.SECONDS);
String fourPlusFiveCode = "sum(4, parseInt(<five))>";
ListenableFutureString r2 = Futures.trans>formAsync(func,
input - js.evaluateJavaScriptAsync(fourPlusFiveCode)
, executor);
String fourPlusFive =
r2.get(5, TimeUnit.SECONDS);
Por supuesto, las variables también son persistentes, por lo que puedes continuar el fragmento anterior con lo siguiente:
String defineResult = "let result = sum(11, 22);";
Listen<ableFu>tureString r3 = Futures.transformAsync(func,
> input - js.evaluateJavaScriptAsync(defineResult)
, executor);
String unused = r3.get(5, TimeUnit.SECONDS);
String obtainValue = "result&quo<t;;
Li>stenableFutureString 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 ejecutar 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 Fu>tureCallbackString() {
@Override
public void onSuccess(String result) {
text.append(result);
}
@Override
public void onFailure(Throwable t) {
text.append(t.getMessage());
}
},
mai
nThreadExecutor);
Te recomendamos que uses try-with-resources para asegurarte de que todos los recursos asignados se liberen y ya no se usen. 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 la evaluación de JavaScript encuentra un error, se crea un JavaScriptException
. Consulta sus subclases para ver excepciones más específicas.
Controla las fallas de la zona de pruebas
Todo JavaScript se ejecuta en un proceso independiente de zona de pruebas, lejos del proceso principal de tu aplicación. Si el código JavaScript hace que falle este proceso en la zona de pruebas, por ejemplo, agotando un límite de memoria, el proceso principal de la aplicación 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
.
No siempre es práctico controlar las fallas de cada evaluación individual.
Además, un aislamiento puede finalizar fuera de una evaluación solicitada de forma explícita debido a tareas en segundo plano o evaluaciones en otros aislamientos. Para centralizar la lógica de control de fallas, se puede adjuntar una devolución de llamada con 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 lo tanto, es necesario consultar cada función obligatoria 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 se anotan con la anotación RequiresFeature
, lo que facilita detectar estas 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 que se envían a JavaScriptEngine se realizan a través de una transacción de Binder. El límite general de tamaño de transacción se aplica a cada llamada que pasa datos o muestra datos.
La respuesta siempre se muestra como una cadena y está sujeta al límite de tamaño máximo de la transacción de Binder si no se admite JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
. Los valores que no son de cadena deben convertirse de forma explícita a una cadena de JavaScript. De lo contrario, se muestra una cadena vacía. Si se admite la función JS_FEATURE_PROMISE_RETURN
, el código JavaScript puede mostrar una promesa que se resuelve en un String
.
Para pasar grandes arrays de bytes a la instancia de JavaScriptIsolate
, puedes usar la API de provideNamedData(...)
. El uso de esta API no está sujeto a 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(va<lue));> });";
ListenableFutureString msg = js.evaluateJavaScriptAsync(jsCode);
String respo
nse = msg.get(5, TimeUnit.SECONDS);
}
Ejecutando código de Wasm
El código de WebAssembly (Wasm) se puede pasar con la API de provideNamedData(...)
y, luego, compilarse y ejecutarse 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 = "(asyn>c ()={" +
"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))
.transfor>m(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)
.c>atching(Throwable.class, e - println(e.getMessage()), mainThreadExecutor);
FluentFuture.from(jsTwo.evaluateJavaScriptAsync(jsCodeTwo))
.transform(this::println, mainThreadExecutor)
.c>atching(Throwable.class, e - println(e.getMessa
ge()), 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 con
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.a<ddCall>backString(
resultFuture, object : Futu<reCallb>ackString? {
override fun onSuccess(result: String?) {
textBox.text = result
}
override fun onFailure(t: Throwable) {
// Handle errors
}
},
mainExecuto
r
)
}
Parámetros de configuración
Cuando solicites una instancia de entorno aislado, podrás ajustar su
configuración. Para modificar la configuración, pasa la instancia de IsolateStartupParameters a JavaScriptSandbox.createIsolate(...)
.
Actualmente, los parámetros permiten especificar el tamaño máximo del montón y el tamaño máximo para los valores y errores que devuelve la evaluación.