JavaScript 및 WebAssembly 실행

JavaScript 평가

Jetpack 라이브러리 JavaScriptEngine은 애플리케이션이 WebView 인스턴스를 만들지 않고 JavaScript 코드를 평가할 수 있습니다.

비대화형 JavaScript 평가가 필요한 애플리케이션의 경우 JavaScriptEngine 라이브러리에는 다음과 같은 이점이 있습니다.

  • WebView를 할당할 필요가 없으므로 리소스 소비 감소 인스턴스를 만들 수 있습니다

  • 서비스 (WorkManager 작업)에서 수행할 수 있습니다.

  • 오버헤드가 낮은 다중 격리 환경 덕분에 애플리케이션이 여러 자바스크립트 코드를 동시에 실행할 수 있습니다.

  • API 호출을 사용하여 대량의 데이터를 전달하는 기능

기본 사용법

시작하려면 JavaScriptSandbox 인스턴스를 만듭니다. 이것은 외부 자바스크립트 엔진에 연결할 수 있습니다

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

샌드박스의 수명 주기를 샌드박스의 수명 주기와 일치시키는 것이 JavaScript 평가가 필요한 구성 요소입니다.

예를 들어 샌드박스를 호스팅하는 구성요소는 Activity 또는 Service 단일 Service를 사용하여 JavaScript 평가를 캡슐화할 수 있습니다. 설계할 수 있습니다

JavaScriptSandbox 인스턴스는 공정하게 할당되므로 유지합니다. 비용이 많이 듭니다. 애플리케이션당 하나의 JavaScriptSandbox 인스턴스만 허용됩니다. IllegalStateException은 애플리케이션이 두 번째 JavaScriptSandbox 인스턴스 그러나 여러 개의 실행 환경이 필요한 경우 여러 JavaScriptIsolate 인스턴스를 할당할 수 있습니다.

샌드박스 인스턴스를 더 이상 사용하지 않으면 닫아 리소스를 확보합니다. 이 JavaScriptSandbox 인스턴스는 AutoCloseable 인터페이스를 구현합니다. 간단한 차단 사용 사례에 try-with-resources 사용을 허용합니다. 또는 JavaScriptSandbox 인스턴스 수명 주기를 다음에서 관리해야 합니다. 호스팅 구성요소로 이동하여 액티비티의 onStop() 콜백에서 닫거나 서비스(onDestroy() 동안:

jsSandbox.close();

JavaScriptIsolate 인스턴스는 실행을 위한 컨텍스트를 나타냅니다. JavaScript 코드 필요한 경우 이러한 리소스를 할당할 수 있으므로 보안이 약해집니다. 출처가 다른 스크립트에 대한 경계 또는 동시 자바스크립트 사용 설정 JavaScript는 본질적으로 단일 스레드이기 때문입니다. 이후 호출 동일한 인스턴스가 동일한 상태를 공유하므로 몇 가지 데이터를 만들어 나중에 JavaScriptIsolate의 동일한 인스턴스에서 처리합니다.

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

close() 메서드를 호출하여 JavaScriptIsolate를 명시적으로 해제합니다. JavaScript 코드를 실행하는 격리 인스턴스 닫기 (불완전한 Future가 있음)의 결과로, IsolateTerminatedException이 발생합니다. 이 구현 시 백그라운드로의 백그라운드 정리가 JS_FEATURE_ISOLATE_TERMINATION를 지원합니다. 샌드박스 비정상 종료 처리 섹션을 참고하세요. 있습니다. 그렇지 않으면 대기 중인 모든 평가가 평가될 때까지 정리가 연기됩니다. 샌드박스가 닫힐 수 있습니다.

애플리케이션은 다음 위치에서 JavaScriptIsolate 인스턴스를 만들고 액세스할 수 있습니다. 할 수 있습니다.

이제 애플리케이션이 일부 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);

잘 작성된 동일한 자바스크립트 코드:

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

코드 스니펫은 String로 전달되고 결과는 String로 전달됩니다. evaluateJavaScriptAsync()를 호출하면 평가된 자바스크립트 코드의 마지막 표현식에 대한 결과입니다. 이 이름은 JavaScript String 유형의 속성입니다. 그렇지 않으면 라이브러리 API가 빈 값을 반환합니다. JavaScript 코드는 return 키워드를 사용하면 안 됩니다. 샌드박스가 특정 기능, 추가 반환 유형 (예: Promise)을 지원합니다. String로 확인되는 코드)가 가능할 수 있습니다.

또한 이 라이브러리는 AssetFileDescriptor 또는 ParcelFileDescriptor입니다. 자세한 내용은 evaluateJavaScriptAsync(AssetFileDescriptor)evaluateJavaScriptAsync(ParcelFileDescriptor)에서 자세한 내용을 확인하세요. 이 API는 디스크 또는 앱의 파일에서 평가하는 데 더 적합합니다. 디렉터리

라이브러리는 디버깅에 사용할 수 있는 콘솔 로깅도 지원합니다. 있습니다. setConsoleCallback()를 사용하여 설정할 수 있습니다.

컨텍스트가 유지되므로 코드를 업로드하고 여러 번 실행할 수 있습니다. 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);

물론, 변수는 영구적이므로 이전 코드를 계속 사용할 수 있습니다. 다음 코드를 추가합니다.

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

예를 들어, 필요한 모든 객체를 할당하고 JavaScript 코드를 실행하는 방법은 다음과 같습니다.

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

try-with-resources를 사용하여 모든 요청이 제대로 할당되었는지 리소스가 해제되고 더 이상 사용되지 않습니다 샌드박스 결과 닫기 모든 JavaScriptIsolate 인스턴스에서 대기 중인 모든 평가에서 실패 SandboxDeadException 포함 JavaScript 평가가 실행될 때 오류가 발생하면 JavaScriptException이 생성됩니다. 서브클래스 참조 를 참조하세요.

샌드박스 비정상 종료 처리

모든 JavaScript는 애플리케이션이 아닌 별도의 샌드박스 프로세스에서 실행되며 기본 프로세스입니다. 자바스크립트 코드로 인해 샌드박스 처리된 프로세스가 애플리케이션의 기본 애플리케이션이 메모리 한도를 소진하거나 변경사항의 영향을 받지 않습니다.

샌드박스 비정상 종료가 발생하면 해당 샌드박스의 모든 격리가 종료됩니다. 가장 모든 평가가 객관적인 데이터를 기반으로 IsolateTerminatedException 상황에 따라 특정 예외(예: SandboxDeadException 또는 MemoryLimitExceededException이 발생할 수 있습니다.

개별 평가마다 비정상 종료를 처리하는 것이 항상 실용적이지는 않습니다. 또한 격리는 명시적으로 요청된 백그라운드 작업 또는 다른 격리의 평가로 인해 평가를 받는 경우가 많습니다 비정상 종료 를 사용하여 콜백을 첨부하여 로직 처리를 중앙 집중화할 수 있습니다. 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);

선택적 샌드박스 기능

기본 WebView 버전에 따라 샌드박스 구현 시 사용할 수 있습니다. 따라서 각 포드에 대한 JavaScriptSandbox.isFeatureSupported(...)를 사용하여 이 기능을 구현할 수 있습니다. 중요한 것은 이러한 기능에 의존하는 메서드를 호출하기 전에 기능 상태를 확인해야 합니다.

일부 국가에서는 사용하지 못할 수도 있는 JavaScriptIsolate 메서드는 찾기 쉽도록 RequiresFeature 주석이 달려 있습니다. 호출이 있을 수 있습니다.

매개변수 전달

JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT가 다음에 해당하는 경우 자바스크립트 엔진으로 전송된 평가 요청이 구속되지 않음 바인더 트랜잭션 한도로 인해 기능이 지원되지 않는 경우 JavaScriptEngine은 바인더 트랜잭션을 통해 발생합니다. 일반 트랜잭션 크기 한도는 데이터 또는 는 데이터를 반환합니다.

응답은 항상 문자열로 반환되며 바인더의 영향을 받습니다. 트랜잭션 최대 크기 제한인 경우 JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT이(가) 아닌 경우 지원됩니다. 문자열이 아닌 값은 자바스크립트 문자열로 명시적으로 변환해야 함 그렇지 않으면 빈 문자열이 반환됩니다. JS_FEATURE_PROMISE_RETURN인 경우 기능이 지원되는 경우 JavaScript 코드가 Promise를 반환할 수도 있습니다. String로 확인됩니다.

대형 바이트 배열을 JavaScriptIsolate 인스턴스에 전달하는 방법은 다음과 같습니다. provideNamedData(...) API를 사용할 수 있습니다. 이 API의 사용은 다음 사항에 의해 구속되지 않습니다. Binder 트랜잭션 한도를 제어합니다. 각 바이트 배열은 고유한 다시 사용할 수 없습니다.

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

Wasm 코드 실행

WebAssembly (Wasm) 코드는 provideNamedData(...) API를 호출하고 일반적인 방식으로 컴파일하고 실행했습니다.

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

JavaScriptIsolate 분리

모든 JavaScriptIsolate 인스턴스는 서로 독립적이며 다음 작업을 수행하지 않습니다. 무엇이든 공유할 수 있습니다. 다음 스니펫은

Hi from AAA!5

다음 URL은

Uncaught Reference Error: a is not defined

jsTwo” 인스턴스가 다음에서 생성된 객체를 볼 수 없기 때문입니다. '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);

Kotlin 지원

이 Jetpack 라이브러리를 Kotlin 코루틴과 함께 사용하려면 종속 항목을 추가합니다. kotlinx-coroutines-guava 이를 통해 ListenableFuture

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

이제 Jetpack 라이브러리 API를 코루틴 범위에서 호출할 수 있습니다. 아래에 나와 있습니다.

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

구성 매개변수

격리된 환경 인스턴스를 요청할 때 구성할 수 있습니다 구성을 조정하려면 IsolateStartupParameters 인스턴스를 사용하여 JavaScriptSandbox.createIsolate(...)

현재 매개변수를 사용하면 최대 힙 크기와 최대 크기를 지정할 수 있습니다. 오류를 확인할 수 있습니다.