הפעלת JavaScript ו-WebAssembly

הערכת JavaScript

ספריית Jetpack JavaScript מאפשרת לאפליקציה לבדוק קוד JavaScript בלי ליצור מופע של WebView.

עבור אפליקציות שמחייבות הערכה לא אינטראקטיבית של JavaScript, יש להשתמש במשתנה לספריית JavaScript יש את היתרונות הבאים:

  • צריכת משאבים נמוכה יותר, כי אין צורך להקצות WebView. מכונה.

  • ניתן לבצע זאת בתוך שירות (משימה של WorkManager).

  • סביבות מבודדות מרובות עם תקורה נמוכה, וכך האפליקציה יכולה להריץ כמה קטעי קוד JavaScript בו-זמנית.

  • יכולת להעביר כמויות גדולות של נתונים באמצעות קריאה ל-API.

שימוש בסיסי

כדי להתחיל, יוצרים מופע של JavaScriptSandbox. הדבר מייצג חיבור למנוע JavaScript שאינו בתהליך.

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

מומלץ להתאים את מחזור החיים של ארגז החול למחזור החיים של שמחייב הערכה של JavaScript.

לדוגמה, רכיב שמארח את ה-Sandbox עשוי להיות Activity או Service. Service יחיד עשוי לשמש להערכת JavaScript לכל רכיבי האפליקציה.

לשמור על המכונה JavaScriptSandbox כי ההקצאה שלה מתבצעת בצורה הוגנת יקרות. בכל אפליקציה יכולה להיות רק מופע אחד של JavaScriptSandbox. IllegalStateException מושלכת כשאפליקציה מנסה להקצות למופע JavaScriptSandbox השני. אבל אם מספר סביבות הפעלה נדרשים, אפשר להקצות כמה מופעים של JavaScriptIsolate.

אם לא משתמשים יותר בו, סוגרים את המופע של ה-Sandbox כדי לפנות משאבים. במכונה JavaScriptSandbox מוטמע ממשק AutoCloseable, שמאפשר לנסות להשתמש במשאבים בתרחישים לדוגמה פשוטים של חסימה. לחלופין, צריך לוודא שמחזור החיים של מכונה JavaScriptSandbox מנוהל על ידי רכיב האירוח, וסוגרים אותו בקריאה החוזרת (callback) של onStop() לפעילות או במהלך onDestroy() עבור שירות:

jsSandbox.close();

מכונה של JavaScriptIsolate מייצגת הקשר לביצוע קוד JavaScript. אפשר להקצות אותם במקרה הצורך, וכך לשפר את האבטחה גבולות לסקריפטים ממקור שונה או שמאפשרים JavaScript בו-זמנית מפני ש-JavaScript הוא חלק משרשור יחיד בטבע. הקריאות הבאות אל לאותו מצב יש את אותו המצב, לכן אפשר ליצור נתונים מסוימים ואז לעבד אותו מאוחר יותר באותו מופע של JavaScriptIsolate.

JavaScriptIsolate jsIsolate = jsSandbox.createIsolate();

שחרר את JavaScriptIsolate באופן מפורש על ידי קריאה ל-method close() שלו. סגירת מכונה מבודדת שמריצה קוד JavaScript (אם השדה Future לא הושלם) התוצאה תהיה IsolateTerminatedException. מתבצע ניקוי של הבידוד ולאחר מכן ברקע, אם ההטמעה שתומך ב-JS_FEATURE_ISOLATE_TERMINATION, כמו שמתואר הקטע טיפול בקריסות Sandbox בהמשך הדף הזה. אחרת, הניקוי נדחה עד שכל ההערכות שבהמתנה יושלם או ש-Sandbox ייסגר.

האפליקציה יכולה ליצור מופע של 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);

אותו קטע קוד JavaScript, בפורמט טוב:

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 של JavaScript; אחרת, ה-Library API יחזיר ערך ריק. קוד ה-JavaScript לא יכול להשתמש במילת מפתח מסוג return. אם מדובר ב-Sandbox תומכת בתכונות מסוימות ובסוגי החזרה נוספים (לדוגמה, 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);

מומלץ להיעזר במשאבי 'התנסות עם משאבים' כדי לוודא שכל החשבונות מוקצים משאבים משוחררים ולא נמצאים בשימוש עוד. סגירת התוצאות של ארגז החול בכל ההערכות הממתינות בכל המופעים של JavaScriptIsolate שנכשלו עם SandboxDeadException. כשמתבצעת הערכה של JavaScript שגיאה, נוצר JavaScriptException. מידע על מחלקות המשנה שלו לקבלת חריגים ספציפיים יותר.

טיפול בקריסות Sandbox

כל JavaScript מבוצע בתהליך ארגז חול נפרד, מחוץ בתהליך המרכזי של האפליקציה. אם קוד ה-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);

תכונות אופציונליות של Sandbox

בהתאם לגרסת ה-WebView הבסיסית, הטמעה של Sandbox עשויה להיות קבוצות שונות של תכונות זמינות. לכן צריך לשלוח שאילתה לגבי כל באמצעות JavaScriptSandbox.isFeatureSupported(...). חשוב כדי לבדוק את הסטטוס של התכונות לפני קריאה ל-methods שמסתמכות על התכונות האלה.

שיטות JavaScriptIsolate שייתכן שלא יהיו זמינות בכל מקום עם הערה RequiresFeature, וכך קל יותר לזהות קריאות בקוד.

פרמטרים מעבירים

אם JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT הוא אם אין תמיכה, בקשות ההערכה שנשלחות למנוע ה-JavaScript לא כפופות בהתאם למגבלות של טרנזקציית קלסר. אם התכונה לא נתמכת, כל הנתונים עבור מנוע JavaScript מתרחש באמצעות טרנזקציה של Binder. ההנחיות הכלליות מגבלת גודל העסקה חלה על כל קריאה שעוברת בחבילת הגלישה או מחזירה נתונים.

התגובה תמיד מוחזרת כמחרוזת והיא כפופה ל-Binnder מגבלת הגודל המקסימלי לעסקה אם JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT אינו נתמך. יש להמיר ערכים שאינם מחרוזת באופן מפורש למחרוזת JavaScript אחרת תוחזר מחרוזת ריקה. אם JS_FEATURE_PROMISE_RETURN התכונה נתמכת, קוד JavaScript עשוי גם להחזיר הבטחה מתבצע מעבר ל-String.

כדי להעביר מערכים של בייטים גדולים למכונה JavaScriptIsolate, צריך: יכולים להשתמש ב-API של provideNamedData(...). השימוש ב-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(...) ולאחר מכן הידור וההרצה של הפעולות מתבצעות כרגיל, כמו שמוצג בהמשך.

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

כל המופעים של JavaScriptIsolate נפרדים זה מזה ולא לשתף כל דבר. קטע הקוד הבא גורם ל:

Hi from AAA!5

וגם

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"
}

עכשיו אפשר לקרוא לממשקי ה-API של ספריית Jetpack מהיקף של Coroutine, הדוגמה הבאה:

// 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(...).

נכון לעכשיו, פרמטרים מאפשרים לציין את גודל הערימה המקסימלי ואת הגודל המקסימלי להערכה של ערכים מוחזרים ושגיאות.