בדף הזה נסביר על השיטות השונות ועל השיטות המומלצות להקמת גשר מקורי, שנקרא גם גשר JavaScript, כדי לאפשר תקשורת בין תוכן מהאינטרנט ב-WebView לבין אפליקציה ל-Android מארחת.
כך מפתחי אתרים יכולים להשתמש ב-JavaScript כדי לגשת לתכונות של פלטפורמה מקומית – כמו המצלמה, מערכת הקבצים או חיישני חומרה מתקדמים – שבדרך כלל לא מסופקות על ידי ממשקי API רגילים לאינטרנט.
תרחישים לדוגמה
הטמעה של גשר JavaScript מאפשרת תרחישי שילוב שונים שבהם תוכן אינטרנט דורש גישה מעמיקה יותר למערכת ההפעלה Android. הנה כמה דוגמאות:
- שילוב פלטפורמות: הפעלה של רכיבי ממשק משתמש מקוריים של Android (לדוגמה, הנחיות ביומטריות,
BottomSheetDialog) מדף אינטרנט. - ביצועים: העברת משימות חישוביות כבדות לקוד Java או Kotlin מקורי.
- התמדת נתונים: גישה למסדי נתונים מוצפנים מקומיים או להעדפות משותפות.
- העברות נתונים גדולות: העברת קובצי מדיה או מבני נתונים מורכבים בין האפליקציה לבין רכיב העיבוד של הדף.
מנגנוני תקשורת
Android מציעה שלושה דורות עיקריים של ממשקי API ליצירת גשר מקורי. הן עדיין זמינות, אבל יש ביניהן הבדלים משמעותיים באבטחה, בנוחות השימוש ובביצועים.
שימוש ב-addWebMessageListener (מומלץ)
addWebMessageListener היא הגישה המודרנית והמומלצת ביותר לתקשורת בין תוכן האינטרנט לבין קוד אפליקציית הנייטיב. הוא משלב את קלות השימוש בממשק JavaScript עם האבטחה של מערכת ההודעות.
איך זה עובד: האפליקציה מוסיפה מאזין עם שם ספציפי וקבוצה של כללי מקור מותרים. לאחר מכן, רכיב ה-WebView מוודא שאובייקט ה-JavaScript נמצא בהיקף הגלובלי (window.objectName) מרגע שהדף מתחיל להיטען.
הפעלה: כדי לוודא שרכיב ה-WebView מזריק את אובייקט ה-JavaScript לפני הפעלת סקריפט כלשהו, צריך לקרוא ל-addWebMessageListener לפני המעבר לדף (למשל, קריאה ל-WebViewCompat.navigate או ל-loadUrl).
התכונות העיקריות:
אבטחה ואמון: בניגוד לממשקי API מדור קודם, השיטה הזו מחייבת
Set<String>שלallowedOriginRulesבמהלך האתחול. זהו המנגנון העיקרי ליצירת אמון.כשמציינים מקור מהימן, כמו
https://example.com, רכיב ה-WebView מבטיח שהוא יחשוף את אובייקטי ה-JavaScript המוזרקים רק לדפי אינטרנט שנטענו מהמקור הזה בדיוק.הקריאה החוזרת של מאזין מקורי מקבלת פרמטר
sourceOriginעם כל הודעה. אפשר להשתמש בזה כדי לאמת את המקור המדויק של השולח אם הגשר תומך בכמה מקורות מותרים.מכיוון ש-WebView אוכף את הבדיקות האלה של המקור ברמת הפלטפורמה, האפליקציה יכולה בדרך כלל להסתמך על הודעות שמתקבלות ממקור מהימן
sourceOriginכהודעות אמיתיות, ולכן ברוב ההטמעות הרגילות אין צורך באימות קפדני של המטען הייעודי.- WebView מתאים כללים לסכמה (HTTP/HTTPS), למארח ולפורט.
- WebView מתעלם מנתיבים. לדוגמה,
https://example.comמאפשרתhttps://example.com/loginו-https://example.com/home. - ב-WebView, השימוש בתווים כלליים מוגבל לתחילת המארח עבור תת-דומיינים. לדוגמה,
https://*.example.comתואם ל-https://foo.example.comאבל לא ל-https://example.com. אם אתם צריכים להתאים גם אתhttps://example.comוגם את תתי-הדומיינים שלו, אתם צריכים להוסיף כל כלל מקור בנפרד לרשימת ההיתרים (לדוגמה,"https://example.com", "https://*.example.com"). אי אפשר להשתמש בתווים כלליים בסכימה או באמצע של דומיין.
ההגדרה הזו מגבילה את הגשר לדומיינים מאומתים, ומונעת מתוכן של צד שלישי לא מורשה או מ-iframe מוחדר להריץ קוד Native.
תמיכה במספר מסגרות: פועל בכל המסגרות שתואמות לכללי המקור.
שרשור: הקריאה החוזרת (callback) של מאזין פועלת בשרשור הראשי (UI) של האפליקציה. אם הגשר צריך לטפל בעיבוד נתונים מורכב, בניתוח של JSON או בחיפושים במסד נתונים, צריך להעביר את העבודה הזו לשרשור ברקע כדי למנוע את קפיאת ממשק המשתמש של האפליקציה עם השגיאה 'האפליקציה לא מגיבה' (ANR).
דו-כיוונית: כשדף האינטרנט שולח הודעה, האפליקציה מקבלת
JavaScriptReplyProxyשאפשר להשתמש בה כדי לשלוח הודעות חזרה למסגרת הספציפית הזו. אפשר לשמור את אובייקטreplyProxyהזה ולהשתמש בו בכל שלב כדי לשלוח מספר כלשהו של הודעות לדף, ולא רק כדי להשיב לכל הודעה שהדף שולח. אם המסגרת המקורית עוברת למקום אחר או נהרסת, המערכת מתעלמת בשקט מהודעות שנשלחות באמצעותpostMessage()בשרת ה-proxy.הפעלה בצד האפליקציה: למרות שדף האינטרנט תמיד צריך להפעיל את ערוץ התקשורת עם האפליקציה, אפליקציית הנייטיב יכולה להציג לדף האינטרנט באופן חד-צדדי הנחיה להתחיל את התהליך הזה. אפליקציית הנייטיב יכולה לתקשר עם דף האינטרנט באמצעות
addDocumentStartJavaScript()(כדי להעריך JavaScript לפני שהדף נטען) אוevaluateJavaScript()(כדי להעריך JavaScript אחרי שהדף נטען).
הגבלה: ה-API הזה שולח נתונים כמחרוזות או כמערכים של byte[]. במבני נתונים מורכבים יותר, כמו אובייקטים של JSON, צריך לבצע סריאליזציה לאחד מהפורמטים האלה ואז לבצע דה-סריאליזציה בצד השני כדי לשחזר את מבנה הנתונים.
דוגמה לשימוש:
כדי להבין את הרצף המלא של חילופי הודעות דו-כיווניים, האירועים מתרחשים בסדר הבא:
- הפעלה (אפליקציה): אפליקציית הנייטיב רושמת את מאזין האירועים ב-
addWebMessageListenerומפעילה את הניווט בדף (למשל באמצעותWebViewCompat.navigateאוloadUrl). - שליחת הודעה (אינטרנט): קריאות JavaScript בדף האינטרנט
myObject.postMessage(message)כדי להתחיל את התקשורת. - קבלת הודעות ושליחת תשובות (אפליקציה): האפליקציה מקבלת את ההודעה ב-callback של listener ועונה באמצעות
replyProxy.postMessage()שסופק. - קבלת תשובה (אינטרנט): דף האינטרנט מקבל את התשובה האסינכרונית בפונקציית הקריאה החוזרת
myObject.onmessage().
Kotlin
val myListener = WebViewCompat.WebMessageListener { _, _, _, _, replyProxy ->
// Handle the message from JS
replyProxy.postMessage("Acknowledged!")
}
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val allowedOrigins = setOf("https://www.example.com")
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener)
}
Java
WebMessageListener myListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Handle the message from JS
replyProxy.postMessage("Acknowledged!");
};
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
Set<String> allowedOrigins = Set.of("https://www.example.com");
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener);
}
קוד ה-JavaScript הבא מדגים את ההטמעה בצד הלקוח של addWebMessageListener, שמאפשרת לתוכן האינטרנט לקבל הודעות מהאפליקציית נייטיב ולשלוח הודעות משלו דרך ה-proxy של myObject.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
שימוש ב-postWebMessage (חלופה)
התכונה הזו הושקה ב-Android כדי לספק חלופה אסינכרונית שמבוססת על העברת הודעות, בדומה ל-window.postMessage באינטרנט.
איך זה עובד: האפליקציה משתמשת ב-WebViewCompat.postWebMessage כדי לשלוח מטען ייעודי (payload)
למסגרת הראשית של דף האינטרנט. כדי ליצור ערוץ תקשורת דו-כיווני, אפשר ליצור WebMessageChannel ולהעביר את אחד מהיציאות שלו עם ההודעה לתוכן האינטרנט.
מאפיינים:
- אסינכרוני: כמו
addWebMessageListener, השיטה הזו משתמשת בהעברת הודעות אסינכרונית, שמבטיחה שדף האינטרנט ימשיך להגיב לאינטראקציות של המשתמשים בזמן שהאפליקציה מעבדת נתונים ברקע. - מודע למקור: אפשר לציין
targetOriginכדי לוודא שרכיב ה-WebView יעביר נתונים רק לאתר מהימן.
מגבלות:
- היקף: ה-API הזה מגביל את התקשורת למסגרת הראשית. הוא לא תומך בפנייה ישירה או בשליחת הודעות ל-iframe.
- הגבלות על URI: אי אפשר להשתמש בשיטה הזו לתוכן שנטען באמצעות מזהי URI מסוג
data:,file:אוloadData(), אלא אם מציינים '*' כמקור היעד. כך כל דף יוכל לקבל את ההודעה. - סיכון לזהות: אין דרך ברורה לאמת את זהות השולח בתוכן האינטרנט. יכול להיות שהודעה שמתקבלת בדף אינטרנט הגיעה מאפליקציית נייטיב או מ-iframe אחר.
משתמשים בשיטה הזו כשצריך ערוץ פשוט ואסינכרוני לנתונים מבוססי-מחרוזת בגרסאות קודמות של Android שלא תומכות ב-addWebMessageListener.
שימוש ב-addJavascriptInterface (גרסה קודמת)
השיטה הכי ישנה כוללת הזרקה של מופע אובייקט מקורי ישירות ל-WebView.
איך זה עובד: מגדירים מחלקה ב-Kotlin או ב-Java, מוסיפים את ההערה @JavascriptInterface לשיטות המותרות ומוסיפים מופע של המחלקה ל-WebView באמצעות addJavascriptInterface(Object, String).
מאפיינים:
- סינכרוני: סביבת ההפעלה של JavaScript נחסמת עד שהשיטה בקוד Android מחזירה ערך.
- בטיחות ב-Thread: המערכת מפעילה שיטות ב-Thread ברקע, ולכן נדרש סנכרון זהיר בצד של Kotlin או Java.
- סיכון אבטחה: כברירת מחדל,
addJavascriptInterfaceזמין לכל מסגרת ב-WebView, כולל iframe. אין בו בקרת גישה שמבוססת על מקור. בגלל ההתנהגות האסינכרונית של WebView, אי אפשר לקבוע באופן בטוח את כתובת ה-URL של המסגרת שמפעילה את הממשק. אסור להסתמך על שיטות כמוWebView.getUrl()לאימות אבטחה, כי אין ערובה לכך שהן מדויקות, והן לא מציינות איזה פריים ספציפי של iframe שלח את הבקשה.
המרות והכפייה של סוגי נתונים
כשמשתמשים ב-addJavascriptInterface, גשר Java שמבוסס על Chromium ממיר סוגי נתונים בין זמן הריצה של JavaScript לבין קוד האפליקציה ל-Android.
כללי ההמרה הבאים חלים על פרמטרים של שיטות ועל ערכים מוחזרים.
מיפוי סוגי פרמטרים (JavaScript ל-Java)
כש-JavaScript מעביר ארגומנטים לשיטת Java או Kotlin עם הערות, ה-bridge מבצע המרה של ערכי JavaScript לסוגי הפרמטרים התואמים ב-Java:
| סוג הפרמטר ב-Java | ערך ארגומנט של JavaScript | התנהגות כפייה |
|---|---|---|
byte, short, int, long |
מספר (מספר שלם) | הערכים מומרים לסוג המספר השלם של היעד. ערכים מחוץ לטווח עוברים המרה בהתאם לכללי ההמרה המספריים הרגילים. |
byte, short, int, long |
NaN |
ההמרה מתבצעת ל-0. |
byte, short, int, long |
Infinity |
ההמרה מתבצעת ל--1 עבור byte ו-short, או ל-Integer.MAX_VALUE ו-Long.MAX_VALUE עבור int ו-long. |
float, double |
מספר | הפונקציה מבצעת המרה לערך הנקודה הצפה (floating-point) המתאים ב-Java. |
float, double |
NaN / Infinity |
ההמרה מתבצעת ל-Float.NaN, ל-Double.NaN, ל-Float.POSITIVE_INFINITY או ל-Double.POSITIVE_INFINITY. |
char |
מספר (מספר שלם) | הומר למיקום התו (code point) המתאים ב-Unicode. |
char |
מספר לא שלם, NaN, Infinity |
ההמרה מתבצעת ל-\u0000. |
boolean |
true / false |
ההמרה מתבצעת ל-Java true או false. |
boolean |
מספר, מחרוזת, אובייקט | הפונקציה מבצעת המרה ל-false (כולל מחרוזות לא ריקות ומספרים שונים מאפס). |
String |
מחרוזת | ערך המחרוזת נשמר. |
String |
מספר, ערך בוליאני | הפורמט הוא ייצוג מחרוזת (לדוגמה, "42", "true", "false"). |
String |
null / undefined |
null עובר המרה ל-Java null; undefined עובר המרה למחרוזת המילולית "undefined". |
String |
אובייקט, ArrayBuffer, TypedArray | הפונקציה מבצעת המרה למחרוזת המילולית "undefined". |
מערך פרימיטיבי (כמו int[], byte[], boolean[]) או String[] |
מערך ([...]) |
הפונקציה ממירה למערך Java חד-ממדי של סוג רכיב היעד. מערכים דלילים ממלאים אינדקסים לא מוקצים בערכי ברירת מחדל (0, false, null). |
מערך פרימיטיבי (כמו int[], byte[]) |
TypedArray (Int8Array, Uint8Array, Int32Array, Float64Array) |
הרכיבים נכפים למערך הפרימיטיבי המתאים של Java. |
מערך רב-ממדי (כמו int[][]) |
מערך מקונן ([[...]]) |
לא נתמך. הערך של פרמטרים של מערך רב-ממדי הוא null. |
ArrayBuffer, DataView |
ArrayBuffer, DataView |
לא אפשרי כמערכים. הערך של מופעים של ArrayBuffer ו-DataView הוא null. |
Object או סוג מותאם אישית |
אובייקט JavaScript ({...}) |
לא נתמך. אובייקטים מילוליים שרירותיים של JavaScript מקבלים את הערך null ב-Java. |
Object או סוג מותאם אישית |
wrapper של אובייקט Java שהוחדר | נתמך (המרת קובץ הלוך ושוב). הפונקציה מעבירה את מופע ה-Java הבסיסי לשיטת ה-Java. הפונקציה יוצרת חריג ב-JavaScript אם סוג ה-Java לא תואם לחתימת הפרמטר. |
סוגים באריזה (כמו Integer, Double, Boolean) |
מספר, ערך בוליאני | לא נתמך. סוגי נתונים פרימיטיביים שמופיעים בתוך תיבות נחשבים לאובייקטים אטומים, והערך שלהם הוא null. |
| כל סוג פרימיטיבי | null / undefined |
הערכים משתנים לערכי ברירת מחדל (0, 0.0, \u0000, false). |
Object, String, מערך |
null |
ההמרה מתבצעת ל-Java null. |
מיפוי סוגי החזרה (Java ל-JavaScript)
כששיטה עם הערה ב-Java או ב-Kotlin מחזירה ערך, הגשר ממיר אותו לסוג JavaScript:
| סוג ההחזרה של Java | ערך JavaScript | JavaScript typeof |
|---|---|---|
boolean |
true / false |
"boolean" |
byte, short, int, long, float, double |
מספר | "number" |
char |
מספר (מיקום תו (code point) של Unicode) | "number" |
String (non-null) |
ערך מחרוזת | "string" |
String (null) |
undefined |
"undefined" |
void |
undefined |
"undefined" |
מערך Java (לדוגמה, int[], String[]) |
undefined |
"undefined". אין תמיכה בערכי החזרה של מערך. השיטה של Java לא מופעלת, ומוחזרת undefined בלי להעלות חריגה. |
| אובייקט Java / סוג בהתאמה אישית (לא null) | רכיב wrapper של אובייקט | "object". יוצר עטיפת JavaScript סביב מופע Java. קוד JavaScript יכול לקרוא לכל שיטה ציבורית באובייקט הזה שמסומנת ב-@JavascriptInterface. |
אובייקט Java / סוג מותאם אישית (null) |
null |
"object" |
פרימיטיב באריזה (כמו Integer, Double) |
רכיב wrapper של אובייקט | "object". הערך מוחזר כעטיפה של אובייקט Java אטום ללא שיטות @JavascriptInterface נגישות, ולכן אי אפשר להשתמש בו ב-JavaScript. |
שיטת הגישה לחברים
גשר ה-JavaScript אוכף כללי גישה וחשיפה מחמירים כדי להגן מפני הרצת קוד לא מכוונת:
- השדות לא נחשפים: אי אפשר לגשת לשדות Java (כולל השדות
publicו-public final) מ-JavaScript, והערך שלהם הואundefined. - דרישה להערה: רק שיטות שמוערות באופן מפורש באמצעות
@JavascriptInterfaceנחשפות ל-JavaScript. - הגבלות על החשיפה: השיטות חייבות להיות
public. השיטותprivateו-protectedאף פעם לא נחשפות ל-JavaScript, גם אם הן כוללות את ההערה@JavascriptInterface. - שיטות סטטיות: שיטות סטטיות שמסומנות ב-
@JavascriptInterfaceניתנות להפעלה מ-JavaScript. - הורשה ודריסה:
@JavascriptInterfaceהערות לא עוברות בירושה כשמחלקת משנה דורסת שיטה. אם מחלקה משנית מבטלת שיטה עם הערה ממחלקת-על, המחלקה המשנית חייבת לכלול באופן מפורש את ההערה@JavascriptInterfaceבשיטה שבוטלה כדי לחשוף אותה ל-JavaScript. שיטות ציבוריות שלא בוטלו שמועברות ממחלקת-על נשארות נגישות אם הן מסומנות בהערה במחלקת-העל. - הגנה מפני השתקפות: שיטות השתקפות רגילות של Java (כמו
getClass()) נחסמות ומוצג חריג של JavaScript כדי למנוע פגיעות בביצוע קוד מרחוק. - העמסת שיטות: יש תמיכה בשיטות Java שעברו העמסה. הגשר פותר קריאות לשיטות על סמך מספר הארגומנטים שהועברו בלבד, ולא מתחשב בסוגי הארגומנטים. הפעלת שיטה עם עומס יתר עם מספר לא תקין של ארגומנטים מעלה חריגה ב-JavaScript. אם לשתי פונקציות עמוסות מדי יש אותו מספר ארגומנטים, אחת מהן תיבחר באופן שרירותי.
סיכום המנגנונים
בטבלה הבאה מופיעה השוואה מהירה בין שלושת המנגנונים העיקריים להטמעה של גשרים מקוריים:
| שיטה | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| הטמעה | אסינכרוני (Listener ב-thread הראשי) | אסינכרוני | סינכרוני |
| אבטחה | הגבוהה ביותר (על בסיס רשימת היתרים) | גבוהה (מודעת למקור) | נמוכה (ללא בדיקות מקור) |
| מורכבות | בינוני | בינוני | פשוט |
| כיוון | דו-כיווני | דו-כיווני | צירוף משתמשים לאפליקציה דרך האתר |
| גרסת WebView מינימלית | גרסה 82 (ו-Jetpack Webkit 1.3.0) | גרסה 45 (ו-Jetpack Webkit 1.1.0) | כל הגירסאות |
| מומלץ | כן | לא | לא |
טיפול בהעברות נתונים גדולות
כשמעבירים נתונים גדולים, כמו מחרוזות של כמה מגה-בייט או קבצים בינאריים, צריך לנהל את הזיכרון בקפידה כדי למנוע שגיאות מסוג ANR (האפליקציה לא מגיבה) או קריסות במכשירים עם 32 ביט. בקטע הזה נדון בטכניקות השונות ובמגבלות שקשורות להעברת כמויות גדולות של נתונים בין אפליקציית המארח לבין תוכן האינטרנט.
העברת נתונים בינאריים באמצעות מערכים של בייטים
בעזרת המחלקה WebMessageCompat, אפשר לשלוח מערכים מסוג byte[] ישירות
במקום לבצע סריאליזציה של נתונים בינאריים למחרוזות Base64. הקידוד ב-Base64 מוסיף תקורת נתונים של כ-33%, ולכן השיטה הזו חסכונית יותר בזיכרון ומהירה יותר.
- יתרון בינארי: העברת נתונים בינאריים כמו קובצי תמונות או אודיו בין אפליקציית הנייטיב לבין תוכן מהאינטרנט.
- מגבלה: גם עם מערכי בייטים, המערכת מעתיקה נתונים מעבר לגבול של תקשורת בין תהליכים (IPC) בין האפליקציה לבין התהליך המבודד שבו WebView משתמש כדי לעבד את תוכן האינטרנט. הפעולה הזו עדיין צורכת כמות משמעותית של זיכרון עבור קבצים גדולים מאוד.
בדוגמאות הקוד הבאות אפשר לראות איך מגדירים את addWebMessageListener בצד של אפליקציית נייטיב כדי לקבל הודעות שמסומנות ב-WebMessageCompat.TYPE_ARRAY_BUFFER, ואיך אפשר לענות באמצעות נתונים בינאריים על ידי בדיקה של WebViewFeature.MESSAGE_ARRAY_BUFFER.
Kotlin
fun setupWebView(webView: WebView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val listener = WebViewCompat.WebMessageListener { view, message, sourceOrigin, isMainFrame, replyProxy ->
// Check if the received message is an ArrayBuffer
if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
val binaryData: ByteArray = message.arrayBuffer
// Process your binary data (image, audio, etc.)
println("Received bytes: ${binaryData.size}")
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
val replyBytes = byteArrayOf(0x01, 0x02, 0x03)
replyProxy.postMessage(replyBytes)
}
}
}
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
setOf("https://example.com"), // Security: restrict origins
listener
)
}
}
Java
public void setupWebView(WebView webView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.WebMessageListener listener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Check if the received message is an ArrayBuffer
if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
byte[] binaryData = message.getArrayBuffer();
// Process your binary data (image, audio, etc.)
System.out.println("Received bytes: " + binaryData.length);
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
byte[] replyBytes = new byte[]{0x01, 0x02, 0x03};
replyProxy.postMessage(replyBytes);
}
}
};
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
Set.of("https://example.com"), // Security: restrict origins
listener
);
}
}
קוד ה-JavaScript הבא מדגים את ההטמעה בצד הלקוח של
addWebMessageListener, שמאפשרת לתוכן האינטרנט לשלוח ולקבל נתונים בינאריים (ArrayBuffer) אל אפליקציית הנייטיב וממנה באמצעות ה-proxy של window.myBridge
שהוכנס בדוגמה הקודמת.
// Function to send an image or binary buffer to the app
async function sendBinaryToApp() {
const response = await fetch('image.jpg');
const buffer = await response.arrayBuffer();
// Check if the injected bridge object exists
if (window.myBridge) {
// You can send the ArrayBuffer directly
window.myBridge.postMessage(buffer);
}
}
// Receiving binary data from the app
if (window.myBridge) {
window.myBridge.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
console.log('Received binary data from App, length:', event.data.byteLength);
// Process the binary data (for example, as a Uint8Array)
const bytes = new Uint8Array(event.data);
console.log('First byte:', bytes[0]);
}
};
}
טעינת נתונים יעילה בקנה מידה גדול
לקבצים גדולים מאוד (מעל 10MB), משתמשים בשיטה shouldInterceptRequest כדי להזרים נתונים:
- דף האינטרנט יוזם קריאה לכתובת URL מותאמת אישית של placeholder
fetch(). לדוגמה,https://app.local/large-file. - אפליקציית Android מיירטת את הבקשה הזו ב-
WebViewClient.shouldInterceptRequest. - האפליקציה מחזירה את הנתונים כ-
InputStream.
האפשרות הזו מאפשרת להזרים נתונים במקטעים במקום לטעון את כל מטען הנתונים לזיכרון בבת אחת.
פונקציית JavaScript הבאה מדגימה את הקוד בצד הלקוח לטעינה יעילה של קובץ בינארי גדול מאפליקציית נייטיב באמצעות קריאה רגילה fetch() לכתובת URL של פלייסהולדר בהתאמה אישית.
async function fetchBinaryFromApp() {
try {
// This URL doesn't need to exist on the internet
const response = await fetch('https://app.local/data/large-file.bin');
if (!response.ok) throw new Error('Network response was not okay');
// For raw binary data:
const arrayBuffer = await response.arrayBuffer();
console.log('Received binary data, size:', arrayBuffer.byteLength);
// Process buffer (for example, new Uint8Array(arrayBuffer))
/*
// OR for an image:
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
document.getElementById('myImage').src = imageUrl;
*/
} catch (error) {
console.error('Fetch error:', error);
}
}
בדוגמאות הקוד הבאות מוצג הצד של אפליקציית הנייטיב, באמצעות השיטה WebViewClient.shouldInterceptRequest ב-Kotlin וב-Java, להזרמת קובץ בינארי גדול על ידי יירוט של כתובת URL של פלייסהולדר מותאם אישית שנדרשה על ידי תוכן מהאינטרנט.
Kotlin
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val url = request?.url ?: return null
// Check if this is our custom placeholder URL
if (url.host == "app.local" && url.path == "/data/large-file.bin") {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
val inputStream: InputStream = context.assets.open("my_data.pb")
// 2. Define Response Headers (Crucial for CORS/Fetch)
val headers = mutableMapOf<String, String>()
headers["Access-Control-Allow-Origin"] = "*" // Allow fetch from any origin
// 3. Return the response
return WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
)
} catch (e: Exception) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request)
}
}
Java
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String urlPath = request.getUrl().getPath();
String host = request.getUrl().getHost();
// Check if this is our custom placeholder URL
if ("app.local".equals(host) && "/data/large-file.bin".equals(urlPath)) {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
InputStream inputStream = getContext().getAssets().open("my_data.pb");
// 2. Define Response Headers (Crucial for CORS/Fetch)
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*"); // Allow fetch from any origin
// 3. Return the response
return new WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
);
} catch (Exception e) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request);
}
});
פעולה בהתאם להמלצות האבטחה
כדי להגן על האפליקציה ועל נתוני המשתמשים, חשוב לפעול לפי ההנחיות האלה כשמטמיעים גשר:
החלת HTTPS: כדי לוודא שתוכן זדוני של צד שלישי לא יוכל להפעיל את הלוגיקה המקורית של האפליקציה, צריך לאפשר תקשורת רק עם מקורות מאובטחים.
להסתמך על כללים לגבי מקור: הדרך הטובה ביותר להתמודד עם בעיות שקשורות למהימנות היא להגדיר בקפדנות את
allowedOriginRulesולבדוק אתsourceOriginשמופיע בקריאה החוזרת של ההודעה. מומלץ להימנע משימוש בתו כללי מלא (*), שתואם לכל המקורות, ככלל המקור היחיד, אלא אם זה ממש הכרחי. השימוש בתווים כלליים לחיפוש בתתי-דומיינים (לדוגמה,*.example.com) עדיין תקף ומאפשר התאמה מאובטחת למספר תתי-דומיינים (לדוגמה,foo.example.com,bar.example.com).הערה: כללים לגבי מקורות מגנים מפני אתרים זדוניים של צד שלישי ומפני iframe מוסתרים, אבל הם לא יכולים להגן מפני פרצות אבטחה מסוג XSS (cross-site scripting) בדומיין המהימן שלכם. לדוגמה, אם דף אינטרנט מציג תוכן שנוצר על ידי משתמשים והוא פגיע ל-XSS מאוחסן, תוקף יכול להריץ סקריפט שפועל כמקור מהימן. מומלץ להחיל אימות על מטען הייעודי (payload) של ההודעות לפני שמבצעים פעולות רגישות בפלטפורמה המקורית.
צמצום שטח הפנים: חשיפה רק של השיטות או הנתונים הספציפיים שדרושים לדף האינטרנט.
בדיקת תכונות בזמן ריצה: ממשקי Bridge API חדשים, כולל
addWebMessageListener, הם חלק מספריית Jetpack Webkit. לכן, תמיד כדאי לבדוק אם יש תמיכה באמצעותWebViewFeature.isFeatureSupported()לפני שמתקשרים אליהם.