JavaScript ve WebAssembly'yi yürütme

JavaScript Değerlendirmesi

Jetpack kitaplığı JavaScriptEngine, bir uygulamanın Web Görünümü örneği oluşturmadan JavaScript kodunu değerlendirmesi için bir yol sağlar.

Etkileşimli olmayan JavaScript değerlendirmesi gerektiren uygulamalarda JavaScriptEngine kitaplığının kullanılması aşağıdaki avantajlara sahiptir:

  • Web Görünümü örneği tahsis edilmesine gerek olmadığından daha düşük kaynak tüketimi.

  • Hizmet (WorkManager görevi) içinde yapılabilir.

  • Uygulamanın düşük ek yüke sahip birkaç JavaScript snippet'ini aynı anda çalıştırabilmesini sağlayan çok sayıda yalıtılmış ortam.

  • API çağrısı kullanarak büyük miktarlarda veri iletme özelliği.

Temel Kullanım

Başlamak için JavaScriptSandbox örneği oluşturun. Bu, işlem olmayan JavaScript motoruyla bir bağlantıyı temsil eder.

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

Korumalı alanın yaşam döngüsünü, JavaScript değerlendirmesi gereken bileşenin yaşam döngüsüyle uyumlu hale getirmeniz önerilir.

Örneğin, korumalı alanı barındıran bileşen bir Activity veya Service olabilir. Tüm uygulama bileşenleri için JavaScript değerlendirmesini kapsamak amacıyla tek bir Service kullanılabilir.

Ayırma işlemi oldukça pahalı olduğu için JavaScriptSandbox örneğini koruyun. Uygulama başına yalnızca bir JavaScriptSandbox örneğine izin verilir. Bir uygulama ikinci bir JavaScriptSandbox örneği ayırmaya çalıştığında IllegalStateException yayınlanır. Bununla birlikte, birden fazla yürütme ortamı gerekirse birkaç JavaScriptIsolate örneği ayrılabilir.

Artık kullanılmadığında kaynakları boşaltmak için korumalı alan örneğini kapatın. JavaScriptSandbox örneği, basit engelleme kullanım alanları için kaynakları deneme kullanımına olanak tanıyan bir AutoCloseable arayüzü uygular. Alternatif olarak, JavaScriptSandbox örnek yaşam döngüsünün barındırma bileşeni tarafından yönetildiğinden emin olun ve bunu bir Etkinlik için onStop() geri çağırmasında veya bir Hizmet için onDestroy() sırasında kapatın:

jsSandbox.close();

JavaScriptIsolate örneği, JavaScript kodunu yürütmek için kullanılan bağlamı temsil eder. JavaScript yapısı tek iş parçacıklı olduğundan, gerektiğinde farklı kaynaklara sahip komut dosyaları için zayıf güvenlik sınırları sağlayarak veya eşzamanlı JavaScript yürütme işlemini mümkün kılarak atanabilirler. Aynı örneğe yapılan sonraki çağrılar aynı durumu paylaşır. Bu nedenle, önce bazı veriler oluşturup daha sonra aynı JavaScriptIsolate örneğinde bu verileri işleyebilirsiniz.

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

JavaScriptIsolate yöntemini close() yöntemini çağırarak açıkça serbest bırakın. JavaScript kodu çalıştıran bir izole örneğinin kapatılması (eksik bir Future olması) IsolateTerminatedException ile sonuçlanır. Uygulama, JS_FEATURE_ISOLATE_TERMINATION özelliğini destekliyorsa bu sayfanın sonraki kısımlarında yer alan korumalı alan kilitlenmelerini işleme bölümünde açıklandığı gibi izolasyon daha sonra arka planda temizlenir. Aksi takdirde, bekleyen tüm değerlendirmeler tamamlanana veya korumalı alan kapatılana kadar temizleme işlemi ertelenir.

Bir uygulama, herhangi bir iş parçacığından JavaScriptIsolate örneği oluşturup bu örneğe erişebilir.

Artık uygulama bazı JavaScript kodları çalıştırmaya hazırdır:

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

Aynı JavaScript snippet'i düzgün şekilde biçimlendirilmiştir:

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

Kod snippet'i String olarak iletilir ve sonuç String olarak yayınlanır. evaluateJavaScriptAsync() çağrısının, JavaScript kodundaki son ifadenin değerlendirilen sonucunu döndürdüğünü unutmayın. Bu, JavaScript String türünde olmalıdır. Aksi takdirde, Library API boş bir değer döndürür. JavaScript kodu, return anahtar kelimesini kullanmamalıdır. Korumalı alan belirli özellikleri destekliyorsa ek döndürme türleri (örneğin, String'a çözümleyen bir Promise) mümkün olabilir.

Kitaplık, AssetFileDescriptor veya ParcelFileDescriptor biçimindeki komut dosyalarının değerlendirilmesini de destekler. Daha fazla bilgi için evaluateJavaScriptAsync(AssetFileDescriptor) ve evaluateJavaScriptAsync(ParcelFileDescriptor) sayfalarını inceleyin. Bu API'ler diskteki veya uygulama dizinlerindeki bir dosyadan değerlendirme yapmaya daha uygundur.

Kitaplık, hata ayıklama amacıyla kullanılabilecek konsol günlük kaydını da destekler. Bu özellik, setConsoleCallback() kullanılarak ayarlanabilir.

Bağlam devam ettiğinden, JavaScriptIsolate kullanım ömrü boyunca kod yükleyebilir ve kodu birkaç kez çalıştırabilirsiniz:

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

Elbette, değişkenler de kalıcı olduğundan önceki snippet'e şunlarla devam edebilirsiniz:

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

Örneğin, gerekli tüm nesneleri ayırmak ve bir JavaScript kodunu yürütmek için kullanılan tam snippet aşağıdaki gibi görünebilir:

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

Ayrılan tüm kaynakların serbest bırakıldığından ve artık kullanılmadığından emin olmak için kaynakları deneyerek kullanmanız önerilir. Korumalı alanın kapatılması, SandboxDeadException ile başarısız olan tüm JavaScriptIsolate örneklerinde bekleyen tüm değerlendirmelere neden olur. JavaScript değerlendirmesi bir hatayla karşılaştığında JavaScriptException oluşturulur. Daha spesifik istisnalar için alt sınıflarına bakın.

Korumalı Alan Kilitlenmelerini Yönetme

Tüm JavaScript kodları, uygulamanızın ana işleminden uzakta, korumalı alana alınmış ayrı bir işlemde yürütülür. JavaScript kodu, korumalı alana alınan bu işlemin (örneğin, bir bellek sınırını tüketerek) kilitlenmesine neden olursa uygulamanın ana işlemi etkilenmez.

Korumalı alan kilitlenmesi, bu korumalı alandaki tüm izolasyonların sonlandırılmasına neden olur. Bunun en bariz belirtisi, IsolateTerminatedException ile tüm değerlendirmelerin başarısız olmaya başlamasıdır. Koşullara bağlı olarak SandboxDeadException veya MemoryLimitExceededException gibi daha spesifik istisnalar tanınabilir.

Her bir değerlendirme için kilitlenmeleri ele almak her zaman pratik olmaz. Dahası, diğer izolasyonlardaki arka plan görevleri veya değerlendirmeler nedeniyle bir izolasyon açık bir şekilde talep edilen değerlendirmenin dışında sonlandırılabilir. Kilitlenme işleme mantığı, JavaScriptIsolate.addOnTerminatedCallback() ile bir geri çağırma eklenerek merkezi hale getirilebilir.

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

İsteğe Bağlı Korumalı Alan Özellikleri

Temel WebView sürümüne bağlı olarak, bir korumalı alan uygulamasında farklı özellik grupları bulunabilir. Bu nedenle, gerekli her özelliği JavaScriptSandbox.isFeatureSupported(...) kullanarak sorgulamanız gerekir. Bu özellikleri temel alan yöntemler çağırmadan önce özellik durumunu kontrol etmek önemlidir.

Her yerde kullanılamayabilecek JavaScriptIsolate yöntemlerine RequiresFeature ek açıklaması eklenerek kod içinde bu çağrıların tespit edilmesini kolaylaştırır.

Parametreleri Geçirme

JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT destekleniyorsa JavaScript motoruna gönderilen değerlendirme istekleri bağlayıcı işlem sınırlarına bağlı olmaz. Özellik desteklenmiyorsa JavaScriptEngine'e gönderilen tüm veriler bir Bağlayıcı işlemi aracılığıyla gerçekleşir. Genel işlem boyutu sınırı, veri aktaran veya veri döndüren her çağrı için geçerlidir.

Yanıt her zaman Dize olarak döndürülür ve JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT desteklenmiyorsa Bağlayıcı işlemi maksimum boyut sınırına tabidir. Dize olmayan değerler açıkça bir JavaScript Dizesine dönüştürülmelidir. Aksi takdirde boş bir dize döndürülür. JS_FEATURE_PROMISE_RETURN özelliği destekleniyorsa JavaScript kodu, alternatif olarak String ile çözümlenen bir Promise döndürebilir.

Büyük bayt dizilerini JavaScriptIsolate örneğine geçirmek için provideNamedData(...) API'yi kullanabilirsiniz. Bu API'nin kullanımı, Bağlayıcı işlem sınırlarına tabi değildir. Her bayt dizisi, yeniden kullanılamayan benzersiz bir tanımlayıcı kullanılarak iletilmelidir.

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 Kodunu Çalıştırma

WebAssembly (Wasm) kodu, provideNamedData(...) API kullanılarak iletilebilir, ardından aşağıda gösterildiği gibi normal şekilde derlenip yürütülebilir.

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

JavaScript Yalıtımı

Tüm JavaScriptIsolate örnekleri birbirinden bağımsızdır ve hiçbir şey paylaşmaz. Aşağıdaki snippet ile

Hi from AAA!5

ve

Uncaught Reference Error: a is not defined

çünkü "jsTwo" örneği, "jsOne" içinde oluşturulan nesneleri göremez.

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 Desteği

Bu Jetpack kitaplığını Kotlin eş yordamlarıyla kullanmak için kotlinx-coroutines-guava'e bir bağımlılık ekleyin. Bu, ListenableFuture ile entegrasyona olanak tanır.

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

Jetpack kitaplığı API'leri artık aşağıda gösterildiği gibi, eş yordam kapsamından çağrılabilir:

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

Yapılandırma Parametreleri

Yalıtılmış bir ortam örneği isterken yapılandırmasını değiştirebilirsiniz. Yapılandırmayı düzenlemek için IsolateStartupParameters örneğini JavaScriptSandbox.createIsolate(...)'e iletin.

Şu anda parametreler, değerlendirme döndürme değerleri ve hataları için maksimum yığın boyutunun ve maksimum boyutun belirtilmesine olanak tanımaktadır.