Avaliação de JavaScript
A biblioteca Jetpack JavaScriptEngine oferece uma maneira de um aplicativo avaliar o código JavaScript sem criar uma instância da WebView.
Para aplicativos que exigem avaliação JavaScript não interativa, o uso da biblioteca JavaScriptEngine tem as seguintes vantagens:
Menor consumo de recursos, já que não é necessário alocar um WebView instância.
Pode ser feito em um serviço (tarefa do WorkManager).
Vários ambientes isolados com baixa sobrecarga, permitindo que o aplicativo execute vários snippets JavaScript simultaneamente.
Capacidade de transmitir grandes quantidades de dados usando uma chamada de API.
Uso básico
Para começar, crie uma instância de JavaScriptSandbox
. Isso representa
ao mecanismo JavaScript fora do processo.
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceAsync(context);
É recomendável alinhar o ciclo de vida do sandbox com o ciclo de vida do componente que precisa de avaliação do JavaScript.
Por exemplo, um componente que hospeda o sandbox pode ser um Activity
ou um
Service
. Um único Service
pode ser usado para encapsular a avaliação do JavaScript
para todos os componentes do aplicativo.
Mantenha a instância JavaScriptSandbox
porque a alocação dela é bastante
cara. Só é permitida uma instância do JavaScriptSandbox
por aplicativo. Uma
IllegalStateException
é gerada quando um aplicativo tenta alocar uma
segunda instância de JavaScriptSandbox
. No entanto, se vários ambientes de execução
forem necessários, várias instâncias de JavaScriptIsolate
poderão ser alocadas.
Quando ela não for mais usada, feche a instância do sandbox para liberar recursos. O
A instância JavaScriptSandbox
implementa uma interface AutoCloseable
, que
permite o uso de tentativa com recursos para casos de uso simples com bloqueio.
Como alternativa, verifique se o ciclo de vida da instância do JavaScriptSandbox
é gerenciado por
componente de hospedagem, fechando-o no callback onStop()
para uma atividade ou
durante onDestroy()
para um Serviço:
jsSandbox.close();
Uma instância de JavaScriptIsolate
representa um contexto para executar
código JavaScript. Eles podem ser alocados quando necessário, fornecendo limites de segurança
fracos para scripts de origem diferente ou permitindo a execução simultânea de JavaScript,
já que o JavaScript é de único encadeamento por natureza. Chamadas subsequentes para
da mesma instância compartilham o mesmo estado. Por isso, é possível criar alguns dados
e processar depois na mesma instância de JavaScriptIsolate
.
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();
Libere JavaScriptIsolate
explicitamente chamando o método close()
.
O fechamento de uma instância isolada que executa código JavaScript
(com um Future
incompleto) resulta em um IsolateTerminatedException
. O
isolamento é limpo posteriormente em segundo plano se a implementação
oferecer suporte a JS_FEATURE_ISOLATE_TERMINATION
, conforme descrito na seção
como lidar com falhas de sandbox mais adiante nesta
página. Caso contrário, a limpeza é adiada até que todas as avaliações pendentes sejam
concluídas ou o sandbox seja fechado.
Um aplicativo pode criar e acessar uma instância de JavaScriptIsolate
de
qualquer linha de execução.
Agora, o aplicativo está pronto para executar alguns códigos 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);
O mesmo snippet de JavaScript formatado corretamente:
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);
O snippet de código é transmitido como um String
e o resultado, como um String
.
Chamar evaluateJavaScriptAsync()
retorna o resultado
avaliado da última expressão no código JavaScript. Ele precisa ser
do tipo String
do JavaScript. Caso contrário, a API da biblioteca vai retornar um valor vazio.
O código JavaScript não pode usar uma palavra-chave return
. Se o sandbox
oferecer suporte a determinados recursos, outros tipos de retorno (por exemplo, um Promise
que se resolve em um String
) poderão ser possíveis.
A biblioteca também oferece suporte à avaliação de scripts na forma de um
AssetFileDescriptor
ou um ParcelFileDescriptor
. Consulte
evaluateJavaScriptAsync(AssetFileDescriptor)
e
evaluateJavaScriptAsync(ParcelFileDescriptor)
para mais detalhes.
Essas APIs são mais adequadas para avaliar usando um arquivo em disco ou no app
diretórios.
A biblioteca também oferece suporte à geração de registros do console, que pode ser usada para fins de
depuração. Isso pode ser configurado usando setConsoleCallback()
.
Como o contexto persiste, é possível fazer upload do código e executá-lo várias vezes
durante a vida útil do 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);
Claro, as variáveis também são persistentes, então você pode continuar com a por:
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 exemplo, o snippet completo para alocar todos os objetos necessários e A execução de um código JavaScript pode ser semelhante ao seguinte:
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 usar "testar com recursos" para garantir que todos os
recursos são liberados e não são mais usados. Como fechar os resultados do sandbox
em todas as avaliações pendentes nas instâncias de JavaScriptIsolate
com falha
com um SandboxDeadException
. Quando a avaliação JavaScript encontra
um erro, uma JavaScriptException
é criada. Consulte as subclasses
para exceções mais específicas.
Como lidar com falhas no sandbox
Todo JavaScript é executado em um processo em sandbox separado, fora da sua o processo principal do aplicativo. Se o código JavaScript causar um erro nesse processo sandbox, por exemplo, esgotando um limite de memória, o processo principal do aplicativo não será afetado.
Uma falha do sandbox faz com que todos os isolados nesse sandbox sejam encerrados. O sintoma mais
óbvio disso é que todas as avaliações vão começar a falhar com
IsolateTerminatedException
. Dependendo das circunstâncias,
exceções específicas, como SandboxDeadException
ou
MemoryLimitExceededException
pode ser gerada.
Nem sempre é prático lidar com falhas para cada avaliação individual.
Além disso, um recurso isolado pode ser encerrado fora de uma avaliação solicitada
explicitamente devido a tarefas em segundo plano ou avaliações em outros recursos isolados. A lógica de
gerenciamento de falhas pode ser centralizada anexando um callback usando
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);
Recursos opcionais do sandbox
Dependendo da versão da WebView, uma implementação de sandbox pode ter
diferentes conjuntos de recursos disponíveis. Portanto, é necessário consultar cada recurso
necessário usando JavaScriptSandbox.isFeatureSupported(...)
. É importante
para verificar o status do recurso antes de chamar métodos que dependem deles.
JavaScriptIsolate
que podem não estar disponíveis em todos os lugares são
com a anotação RequiresFeature
, facilitando a detecção desses
no código.
Parâmetros de transmissão
Se o JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
for
compatível, as solicitações de avaliação enviadas ao mecanismo JavaScript não serão vinculadas
pelos limites de transação do vinculador. Se o recurso não for suportado, todos os dados a
JavaScriptEngine ocorre por meio de uma transação de vinculação. O limite geral
de tamanho de transação é aplicável a todas as chamadas que transmitem ou
retornam dados.
A resposta é sempre retornada como uma string e está sujeita à
o limite máximo de tamanho da transação se
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
não é
suporte. Os valores sem string precisam ser explicitamente convertidos em uma string JavaScript.
caso contrário, uma string vazia será retornada. Se o recurso JS_FEATURE_PROMISE_RETURN
tiver suporte, o código JavaScript poderá retornar uma promessa
resolvida para um String
.
Para transmitir grandes matrizes de bytes para a instância JavaScriptIsolate
,
use a API provideNamedData(...)
. O uso dessa API não é vinculado aos
limites de transação do Binder. Cada matriz de bytes precisa ser transmitida usando um
identificador que não pode ser reutilizado.
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);
}
Como executar o código Wasm
O código WebAssembly (Wasm) pode ser transmitido usando o método provideNamedData(...)
API do BigQuery, então compilada e executada da maneira usual, como demonstrado abaixo.
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);
}
Separação JavaScript-Isolar
Todas as instâncias de JavaScriptIsolate
são independentes umas das outras e não
compartilham nada. O snippet a seguir resulta em
Hi from AAA!5
e
Uncaught Reference Error: a is not defined
porque a instância "jsTwo
" não tem visibilidade dos objetos criados em
"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);
Suporte ao Kotlin
Para usar essa biblioteca do Jetpack com corrotinas do Kotlin, adicione uma dependência ao
kotlinx-coroutines-guava
Isso permite a integração com
ListenableFuture
:
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.6.0"
}
As APIs da biblioteca do Jetpack agora podem ser chamadas em um escopo de corrotina, como demonstrados abaixo:
// 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 configuração
Ao solicitar uma instância de ambiente isolada, você pode ajustar seu
configuração do Terraform. Para ajustar a configuração, transmita a
instância IsolateStartupParameters para
JavaScriptSandbox.createIsolate(...)
.
Atualmente, os parâmetros permitem especificar o tamanho máximo do heap e o tamanho máximo para valores de retorno de avaliação e erros.