وب ویو و حافظه

وب ویو یک کامپوننت قدرتمند است که به شما امکان نمایش محتوای وب را در برنامه اندروید خود می‌دهد. با این حال، از آنجا که اساساً یک موتور مرورگر کامل (Chromium) است، حافظه قابل توجهی را اشغال می‌کند و معماری چندپردازشی پیچیده‌ای دارد.

پیشینه فنی: معماری چند فرآیندی

در دستگاه‌های مدرن مبتنی بر اندروید، WebView از یک مدل چند پردازشی برای بهبود امنیت و پایداری استفاده می‌کند. وقتی برنامه شما از WebView استفاده می‌کند، حافظه بین فرآیندهای مختلف توزیع می‌شود:

  1. فرآیند مرورگر (فرآیند برنامه) : این فرآیند اصلی برنامه شماست. این فرآیند شامل شیء Java WebView و بخش "مرورگر" موتور Chromium است. این فرآیند رابط کاربری، درخواست‌های شبکه و رندر GPU (که مستقیماً با خط لوله رندر HWUI اندروید ادغام شده است) را مدیریت می‌کند. برخلاف کروم، WebView فرآیند GPU جداگانه‌ای ندارد.
  2. فرآیند رندرکننده : این فرآیند مسئول تجزیه HTML، اجرای جاوا اسکریپت و طرح‌بندی است. این فرآیند برای امنیت از بقیه سیستم جدا شده است. در حال حاضر، برنامه‌ها فقط یک فرآیند رندرکننده برای همه WebViewها دریافت می‌کنند (به جز چند مورد خاص نادر)، برخلاف Chrome که اغلب از فرآیندهای رندرکننده جداگانه برای سایت‌های مختلف استفاده می‌کند.

معماری وب ویو

چرا این برای حافظه مهم است؟

وقتی از dumpsys meminfo <your_package> استفاده می‌کنید، فقط حافظه‌ای را که توسط فرآیند مرورگر (فرآیند برنامه شما) استفاده می‌شود، می‌بینید. حافظه‌ای که توسط فرآیند رندرکننده استفاده می‌شود، جداگانه محاسبه می‌شود.

در داخل فرآیند مرورگر، حافظه WebView به صورت زیر توزیع می‌شود:

  • Java heap : شامل پوشش‌دهنده‌ی جاوای WebView و اشیاء مرتبط است.
  • Native heap : شامل ساختارهای داده داخلی، حافظه‌های پنهان و وضعیت موتور مرورگر Chromium است. توجه داشته باشید که به دلیل استفاده از PartitionAlloc، برخی از تخصیص‌های بومی WebView ممکن است در dumpsys meminfo تحت عنوان "Native Heap" شمارش نشوند و در عوض تحت عنوان "Other" یا "Unknown" ظاهر شوند.
  • حافظه مشترک : برای اشتراک‌گذاری بافرهای گرافیکی و سایر داده‌ها استفاده می‌شود. این ممکن است به وضوح توسط dumpsys meminfo طبقه‌بندی نشده باشد.

ابزارهای عیب‌یابی

ابزارهای توسعه کروم

قدرتمندترین ابزار برای تجزیه و تحلیل حافظه درون WebView (فرآیند رندر) Chrome DevTools است.

  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://inspect/#devices بروید.

  4. برنامه خود را پیدا کنید و روی inspect کلیک کنید.

  5. در پنجره‌ی DevTools، به تب Memory بروید تا از حافظه‌ی heap snapshot بگیرید یا جدول زمانی تخصیص حافظه برای حافظه‌ی heap جاوا اسکریپت را ثبت کنید.

اطلاعات حافظه دامپسیس

برای مشاهده‌ی میزان حافظه‌ی اشغال شده، adb shell dumpsys meminfo --all <package> استفاده کنید. در خروجی به دنبال دسته‌بندی WebView و تعداد اشیاء آن بگردید.

پروفایل‌بندی رندرکننده

از آنجایی که رندرکننده در یک فرآیند جداگانه اجرا می‌شود، نمی‌توانید فقط با پروفایل کردن برنامه خود، هیپ بومی آن را پروفایل کنید. شما باید PID فرآیند رندرکننده را به طور خاص شناسایی کنید.

برای شناسایی PID رندرکننده صحیح هنگام فعال بودن چندین 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 تلاش می‌کند تا اطمینان حاصل کند که نمونه‌ها می‌توانند زباله‌روب (garbage collector) شوند و تمام منابع خود را به طور خودکار آزاد کنند، اما تضمین این امر در ۱۰۰٪ موارد دشوار است. حتی زمانی که زباله‌روب خودکار کار می‌کند، ممکن است به طور قابل توجهی به تأخیر بیفتد و باعث شود برنامه منابع را بسیار طولانی‌تر از حد انتظار نگه دارد.

اگر برنامه‌ای در زمان مناسب (مثلاً در Activity.onDestroy() ) تابع WebView.destroy() را فراخوانی کند، نگه‌داشتن یک ارجاع به خود شیء WebView هیچ منبع بومی قابل توجهی را فاش نمی‌کند. نیازی به حذف ارجاعات به شیء WebView در فیلدهای Activity پس از از بین بردن آن نیست، زیرا وقتی خود Activity جمع‌آوری زباله می‌شود، این ارجاعات پاک می‌شوند.

تمرین‌ها: کار عملی با حافظه WebView

تمرین ۱: مشاهده‌ی ردپای چندفرایندی

  1. MemoryLab را اجرا کنید و یک اندازه‌گیری پایه از حافظه برنامه خود انجام دهید:

    adb shell dumpsys meminfo com.android.memorylab
    

    نمونه خط مبنا (رنجو): TOTAL PSS: 18915 KB

  2. روی راه‌اندازی وب‌ویو (عادی) ضربه بزنید.

  3. در WebView، چندین بار روی Allocate JS Memory (1000 DIV) ضربه بزنید.

  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. به مجموع PSS بالای فرآیند رندرکننده توجه کنید. در اجرای نمونه ما، پس از چند تخصیص، به حدود ۵۵ مگابایت رسید. توجه داشته باشید که تخصیص‌های جاوا اسکریپت (که توسط موتور V8 مدیریت می‌شوند) معمولاً به بخش‌های Private Other یا Unknown (mmap) از dumpsys meminfo کمک می‌کنند، نه به Dalvik Heap.

تمرین ۲: نشت WebView سمت جاوا

یک اشتباه رایج، نگه‌داشتن یک نمونه WebView در یک فیلد استاتیک یا در یک شیء با عمر طولانی است که نشت می‌کند. از آنجا که شیء WebView یک "لنگر" سنگین است که منابع بومی و احتمالاً کل فرآیندهای رندر را نگه می‌دارد، نشت آن بسیار پرهزینه است.

تأثیر نشت وب‌ویو

  1. در MemoryLab ، روی Launch WebView (Java Leak) ضربه بزنید.
  2. این فعالیت پس از بارگذاری صفحه به طور خودکار بسته می‌شود (شبیه‌سازی پیمایش مکرر و انباشت نشت).
  3. ۴ بار روی دکمه ضربه بزنید.
  4. تعداد نمونه‌های WebView را در برنامه خود بررسی کنید:

    adb shell dumpsys meminfo com.android.memorylab
    

    به دنبال بخش اشیاء در پایین باشید. خواهید دید که تعداد WebViews به ۴ افزایش یافته است.

    نمونه خروجی (۴ نمونه لو رفته) در رنگو:

     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 در مسیر خود ندارید، می‌توانید آن را از درخت اندروید بسازید:

    # 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

  8. روی یکی از نمونه‌های WebView لو رفته کلیک کنید. به پایین اسکرول کنید تا به بخش Sample Path from GC Root برسید . خواهید دید که توسط لیست sLeakedWebViews در com.android.memorylab.WebViewActivity نگهداری می‌شود.

    مسیر AHAT به ریشه GC


← بومی | ↑ بالا | کد برنامه →