Avaliação de JavaScript
A biblioteca JavaScriptEngine do Jetpack oferece uma maneira de um aplicativo avaliar o código JavaScript sem criar uma instância de WebView.
Para aplicativos que exigem avaliação JavaScript não interativa, usar o método A biblioteca JavaScriptEngine tem as seguintes vantagens:
Menor consumo de recursos, já que não é necessário alocar um WebView instância.
Pode ser feita em um serviço (tarefa WorkManager).
Vários ambientes isolados com baixa overhead, permitindo que o aplicativo executar vários snippets de 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);
É recomendado alinhar o ciclo de vida do sandbox com o ciclo de vida do 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.
Manter a instância JavaScriptSandbox
porque sua alocação é bastante
caro. Só é permitida uma instância do JavaScriptSandbox
por aplicativo. Um
IllegalStateException
é gerado quando um aplicativo tenta alocar um
segunda instância de JavaScriptSandbox
. No entanto, se vários ambientes de execução
são necessárias, várias instâncias JavaScriptIsolate
podem ser alocadas.
Quando ela não for mais usada, feche a instância do sandbox para liberar recursos. A
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 JavaScriptIsolate
representa um contexto para executar
Código JavaScript. Eles podem ser alocados quando necessário, oferecendo pouca segurança
limites para scripts de diferentes origens ou ativação de JavaScript simultâneo
já que o JavaScript tem um único thread 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()
dele.
Como fechar uma instância isolada executando código JavaScript
(ter um Future
incompleto) resulta em uma IsolateTerminatedException
. A
O isolamento é limpo posteriormente em segundo plano se a implementação
é compatível com JS_FEATURE_ISOLATE_TERMINATION
, conforme descrito nas
seção Como lidar com falhas no sandbox mais adiante
página. Caso contrário, a limpeza será adiada até que todas as avaliações pendentes sejam
concluído ou a sandbox está fechada.
Um aplicativo pode criar e acessar uma instância do JavaScriptIsolate
pela
qualquer thread.
Agora, o aplicativo está pronto para executar 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 os valores
da última expressão no código JavaScript. Deve ser
do tipo String
JavaScript; Caso contrário, a API da biblioteca retornará um valor vazio.
O código JavaScript não deve usar uma palavra-chave return
. Se a sandbox
oferece suporte a determinados recursos e outros tipos de retorno (por exemplo, um Promise
que se resolve em um String
).
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 ao registro do console, que pode ser usado para depuração
propósitos. Isso pode ser configurado usando setConsoleCallback()
.
Como o contexto persiste, você pode 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 em todas as 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.
Tratamento de falhas do sandbox
Todo JavaScript é executado em um processo em sandbox separado, fora da sua o processo principal do aplicativo. Se o código JavaScript gerar esse processo no sandbox para falhar, por exemplo, esgotando um limite de memória, a principal e o processo não serão afetados.
Uma falha no sandbox fará com que todos os isolamentos dessa sandbox sejam encerrados. A maior
um sintoma óbvio disso é que todas as avaliações vão falhar
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 isolamento pode ser encerrado fora de uma zona
devido a tarefas em segundo plano ou avaliações em outros isolamentos. A falha
de processamento de dados 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 do WebView, uma implementação de sandbox pode ter
diferentes conjuntos de recursos disponíveis. Portanto, é necessário consultar cada
recurso 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 JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT
for
suportado, as solicitações de avaliação enviadas ao mecanismo JavaScript não são vinculadas
pelos limites de transação do binder. Se o recurso não for suportado, todos os dados a
JavaScriptEngine ocorre por meio de uma transação de vinculação. A equipe
limite de tamanho da transação se aplica a todas as chamadas que transmitem dados ou
retorna 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 JS_FEATURE_PROMISE_RETURN
for compatível, o código JavaScript poderá retornar uma promessa
resolver para um String
.
Para transmitir matrizes de bytes grandes à instância JavaScriptIsolate
,
podem usar a API provideNamedData(...)
. O uso dessa API não está sujeito aos
os limites de transação do binder. Cada matriz de bytes precisa ser transmitida usando uma string
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
compartilhar qualquer coisa. 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, passe o valor-chave
IsolateStartupParameters para
JavaScriptSandbox.createIsolate(...)
.
No momento, os parâmetros permitem especificar os tamanhos máximos de heap para valores de retorno de avaliação e erros.