‫WebView וזיכרון

‫WebView הוא רכיב רב עוצמה שמאפשר להציג תוכן מהאינטרנט באפליקציה ל-Android. עם זאת, מכיוון שמדובר למעשה במנוע דפדפן (Chromium) עם תכונות מלאות, הוא תופס הרבה הזיכרון שבשימוש וכולל ארכיטקטורה מורכבת של ריבוי תהליכים.

רקע טכני: ארכיטקטורה מרובת תהליכים

במכשירים מודרניים עם Android, רכיב WebView משתמש במודל מרובה תהליכים כדי לשפר את האבטחה והיציבות. כשהאפליקציה משתמשת ב-WebView, הזיכרון מתחלק בין תהליכים שונים:

  1. תהליך הדפדפן (תהליך האפליקציה): זהו התהליך הראשי של האפליקציה. הוא מכיל את אובייקט Java‏ WebView ואת החלק 'browser' במנוע Chromium. התהליך הזה מנהל את ממשק המשתמש, את בקשות הרשת ואת עיבוד ה-GPU (משולב ישירות עם צינור העיבוד של Android HWUI). בניגוד ל-Chrome, ל-WebView אין תהליך GPU נפרד.
  2. תהליך הרינדור: התהליך הזה אחראי לניתוח של HTML, להרצה של JavaScript ולפריסה. הוא מבודד משאר המערכת מטעמי אבטחה. בשלב הזה, האפליקציות מקבלות רק תהליך עיבוד אחד לכל רכיבי ה-WebView (חוץ מכמה מקרים מיוחדים נדירים), בניגוד ל-Chrome, שלרוב משתמש בתהליכי עיבוד נפרדים לאתרים שונים.

ארכיטקטורה של WebView

למה זה חשוב לזיכרון

כשמשתמשים ב-dumpsys meminfo <your_package>, רואים רק את הזיכרון שבו נעשה שימוש על ידי תהליך הדפדפן (תהליך האפליקציה). הזיכרון שבו נעשה שימוש בתהליך הרינדור נספר בנפרד.

בתוך תהליך הדפדפן, הזיכרון של WebView מחולק באופן הבא:

  • ערימת Java: מכילה את WebView Java wrapper ואובייקטים קשורים.
  • Native heap: מכיל את המבנים הפנימיים של נתוני מנוע הדפדפן Chromium, מטמונים ומצב. שימו לב: בגלל השימוש ב-PartitionAlloc, יכול להיות שחלק מהקצאות הזיכרון המקומיות של WebView לא ייספרו בקטע Native Heap ב-dumpsys meminfo, ויכול להיות שהן יופיעו במקום זאת בקטע Other או Unknown.
  • זיכרון משותף: משמש לשיתוף מאגרי נתונים גרפיים ונתונים אחרים. יכול להיות שהסיווג של התוכן הזה לא יהיה ברור ב-dumpsys meminfo.

כלים לפתרון בעיות

כלי פיתוח ל-Chrome

הכלי הכי יעיל לניתוח הזיכרון בתוך ה-WebView (תהליך הרינדור) הוא כלי הפיתוח ל-Chrome.

  1. הפעלת ניפוי באגים ב-WebView באפליקציה:

    // NOTE: In production, this should be gated behind a developer setting
    // or only enabled for debuggable builds to prevent reverse engineering.
    WebView.setWebContentsDebuggingEnabled(true);
    
  2. מחברים את המכשיר באמצעות USB.

  3. פותחים את Chrome במחשב המארח ועוברים אל chrome://inspect/#devices.

  4. מאתרים את האפליקציה ולוחצים על בדיקה.

  5. בחלון DevTools, עוברים לכרטיסייה Memory כדי לצלם תמונות מצב של הערימה או כדי להקליט צירי זמן של הקצאה עבור ערימת ה-JavaScript.

dumpsys meminfo

אפשר להשתמש ב-adb shell dumpsys meminfo --all <package> כדי לראות פירוט של הזיכרון. מחפשים את הקטגוריה WebView בפלט ואת ספירת האובייקטים.

יצירת פרופיל של מנוע הרינדור

מכיוון שרכיב ה-Renderer פועל בתהליך נפרד, אי אפשר ליצור פרופיל של ה-Heap המקורי שלו רק על ידי יצירת פרופיל של האפליקציה. צריך לזהות את ה-PID של תהליך ה-Renderer באופן ספציפי.

כדי לזהות את מזהה התהליך הנכון של רכיב הרינדור כשכמה רכיבי WebView פעילים:

  1. שימוש ב-dumpsys activity:

    adb shell dumpsys activity processes <your_package_name>
    

    מחפשים את הקטע mConnections. תופיע הודעה ConnectionRecord שמציינת שהאפליקציה מקושרת אל SandboxedProcessService. ‫PID של התהליך הזה הוא מנוע הרינדור. לדוגמה:

    mConnections:
      - ConnectionRecord{... com.android.memorylab/org.chromium.content.app.SandboxedProcessService0:0 ...}
    
  2. בדיקת שמות התהליכים: בדרך כלל תהליכי הרינדור נקראים com.google.android.webview:sandboxed_processX או משהו דומה. אם רק אפליקציה אחת משתמשת ב-WebView, סביר להניח שיהיה רק מופע אחד.

אחרי שמקבלים את ה-PID, אפשר ליצור פרופיל באמצעות heapprofd.

שיטות מומלצות לשימוש בזיכרון ב-WebView

השמדה מפורשת

האפליקציות צריכות להתקשר אל WebView.destroy() כדי לציין מתי הן מסיימות עם מופע.

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

אם אפליקציה קוראת ל-WebView.destroy() בזמן המתאים (למשל, ב-Activity.onDestroy()), שמירת הפניה לאובייקט WebView עצמו לא תגרום לדליפת משאבים מקוריים משמעותית. אין צורך להגדיר הפניות לאובייקט WebView בשדות של Activity כ-null אחרי השמדה שלו, כי המערכת תנקה אותו כשהיא תבצע איסוף אשפה של Activity עצמו.

תרגילים: התנסות מעשית עם זיכרון WebView

תרגיל 1: התבוננות בטביעת הרגל של ריבוי תהליכים

  1. מפעילים את MemoryLab ומבצעים מדידת בסיס של הזיכרון של האפליקציה:

    adb shell dumpsys meminfo com.android.memorylab
    

    ערך בסיסי לדוגמה (rango): TOTAL PSS: 18915 KB

  2. מקישים על הפעלת WebView (רגיל).

  3. בתצוגת WebView, מקישים על Allocate JS Memory (1000 DIVs) כמה פעמים.

  4. בודקים שוב את הזיכרון של האפליקציה:

    adb shell dumpsys meminfo com.android.memorylab
    
  5. שימו לב שהזיכרון בתהליך של האפליקציה לא גדל באופן משמעותי בהשוואה לנתוני הבסיס. הסיבה לכך היא שרכיבי ה-DOM נמצאים בתהליך העיבוד.

  6. מאתרים את תהליך העיבוד:

    adb shell ps -A | grep webview | grep sandboxed
    

    פלט לדוגמה:

    u0_i9002     14227  1087    1632732 135880 do_epoll_wait       0 S com.google.android.webview:sandboxed_process0
    
  7. בודקים את הזיכרון של תהליך הרינדור (באמצעות ה-PID שלו):

    adb shell dumpsys meminfo 14227
    
  8. שימו לב לערך הגבוה של TOTAL PSS בתהליך הרינדור. בדוגמה שלנו, גודל הזיכרון גדל ל-‎~55MB אחרי כמה הקצאות. שימו לב שהקצאות של JavaScript (שמטופלות על ידי מנוע V8) בדרך כלל תורמות לקטעים Private Other או Unknown (mmap) של dumpsys meminfo, ולא ל-Dalvik Heap.

תרגיל 2: דליפת WebView בצד Java

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

ההשפעה של דליפת נתונים ב-WebView

  1. ב-MemoryLab, מקישים על Launch WebView (Java Leak) (הפעלת WebView (דליפת Java)).
  2. הפעילות תיסגר אוטומטית אחרי שהדף ייטען (סימולציה של ניווט חוזר ושל הצטברות דליפות).
  3. מקישים על הכפתור 4 פעמים.
  4. בודקים את מספר המופעים של WebView באפליקציה:

    adb shell dumpsys meminfo com.android.memorylab
    

    מחפשים את הקטע אובייקטים בתחתית. מספר המשתמשים ב-WebViews יגדל ל-4.

    פלט לדוגמה (4 מקרים של דליפת נתונים) ב-rango:

     Objects
               Views:       51         ViewRootImpl:        5
         AppContexts:       14           Activities:        5
              Assets:       38        AssetManagers:        0
       Local Binders:       55        Proxy Binders:       77
       Parcel memory:       41         Parcel count:       68
    Death Recipients:        3             WebViews:        4
    
  5. מבצעים תמונת מצב של הזיכרון ומשתמשים ב-AHAT כדי למצוא את הדליפה. אם אין לכם ahat בנתיב, תוכלו ליצור אותו מעץ Android:

    # Dump heap from device
    adb shell am dumpheap com.android.memorylab /data/local/tmp/heap.hprof
    adb pull /data/local/tmp/heap.hprof
    # Run ahat using the built JAR (found in out/host/linux-x86/framework/)
    java -jar out/host/linux-x86/framework/ahat.jar -p 8888 heap.hprof
    
  6. בממשק האינטרנט של AHAT ‏ (localhost:8888), לוחצים על הקישור allocations (או sites) בתפריט העליון כדי לראות את השימוש הכולל בזיכרון.

    הקצאות של AHAT

  7. מחפשים את הכיתה android.webkit.WebView. לוחצים על מספר המופעים כדי לראות את כל המופעים הפעילים. ברשימה אמורות להופיע כמה מכונות.

    AHAT WebView Instances

  8. לוחצים על אחד מהמופעים של WebView שנחשפו. גוללים למטה לקטע Sample Path from GC Root (נתיב לדוגמה משורש GC). הוא יופיע ברשימה sLeakedWebViews ב-com.android.memorylab.WebViewActivity.

    נתיב AHAT אל שורש GC


← מותאמת | ↑ למעלה | קוד האפליקציה →