مدیریت و تشخیص خرابی حافظه «وب‌نما»

WebView کد بومی را در چندین فرایند اجرا می‌کند تا محتوای وب را در برنامه Android شما پرداز کند. رها کردن نمونه‌های WebView بدون مدیریت می‌تواند منجر به نشت حافظه، خرابی‌های کمبود حافظه (OOM)، و عملکرد ضعیف برنامه شود.

این سند مدل حافظه چند فرایندی WebView را توضیح می‌دهد، نحوه مدیریت صحیح چرخه حیات آن برای جلوگیری از نشت را شرح می‌دهد، و گردش‌های کار عملی برای تشخیص مشکلات حافظه ارائه می‌دهد.

آشنایی با معماری حافظه «وب‌نما»

برای مدیریت مؤثر حافظه WebView، با نحوه تخصیص منابع توسط Android برای محتوای وب آشنا شوید:

  • اجرای چندپردازشی: در Android 8.0 (سطح API 26) و بالاتر، WebView محتوای وب را از عملکردهای اصلی برنامه‌تان در چندین پردازش جدا می‌کند (در دستگاه‌های با حافظه دسترسی تصادفی پایین، ممکن است به یک پردازش واحد برگردد):

    • فرایند میزبان (مرورگر): فرایند اصلی برنامه که در آن Activity و کد Java یا Kotlin شما اجرا می‌شود.
    • فرایند «پردازنده مجزا»: فرایند جعبه ایمنی جداگانه‌ای (SandboxedProcessService) که HTML و CSS را تجزیه می‌کند، جاوا اسکریپت را اجرا می‌کند، و صفحات وب را پرداز می‌کند.
  • ردپای حافظه بومی: بیشتر حافظه WebView، ازجمله گرافیک‌های پردازشی، درخت DOM، و حافظه زمان اجرای جاوا اسکریپت، در حافظه بومی تخصیص داده می‌شود، نه در پشته Java. رونوشت پشته جاوا (.hprof) فقط شیء بسته‌بندی جاوا سبک‌وزن را نشان می‌دهد و حافظه واقعی استفاده‌شده توسط محتوای وب را ضبط نمی‌کند.

  • تأثیر سیستم بر حافظه بومی: برخلاف تخصیص‌های پشته Java که با حد maxHeap برنامه محدود می‌شوند و با OutOfMemoryError به‌سرعت ازکار می‌افتند، حافظه بومی می‌تواند بی‌صدا تا گیگابایت رشد کند. وقتی حافظه بومی منتشرنشده حافظه RAM فیزیکی و فضای مبادله (zRAM) را پر می‌کند، «قاتل حافظه کم» (LMK) در Android شروع به بستن فرایندهای پس‌زمینه‌ای می‌کند تا حافظه را پس بگیرد. این کار باعث کاهش عملکرد کلی چندوظیفگی دستگاه می‌شود و درنهایت برنامه پیش‌زمینه را می‌بندد.

مدیریت چرخه حیات WebView

مدیریت صحیح چرخه حیات برای جلوگیری از نشت حافظه بسیار مهم است. اشتباه رایج این است که تصور کنید برداشتن WebView از چیدمان یا اجازه دادن به Activity برای اتمام خودکار، حافظه آن را آزاد می‌کند.

برای اطمینان از پاک‌سازی کامل هم مرجع‌های زمینه‌ای Java و هم منابع رندر بومی، باید یک توالی برچیدن را به‌طور صریح در چرخه حیات مؤلفه میزبان خود (مثل onDestroy()) هماهنگ کنید، اجرای صفحه فعال را متوقف کنید، نما را از محتوی آن جدا کنید، و پیوندهای بومی را آزاد کنید.

پاکسازی نمونه‌های «وب‌نما»

برای اطمینان از خاموش شدن صحیح و آزاد شدن منابع هنگام ازبین رفتن Activity یا Fragment، مراحل زیر را انجام دهید:

  1. ‫WebView را از محتوی والد آن (ViewGroup) بردارید.
  2. بارگیری فعال متوقف شود و سابقه ناوبری پاک شود.
  3. تماس با destroy().
  4. مرجع null را پاک کنید.

مثال زیر نشان می‌دهد که چگونه یک WebView را به‌درستی پاکسازی کنید:

کاتلین

override fun onDestroy() {
    myWebView?.let {
        // Remove the WebView from its parent ViewGroup.
        (it.parent as? ViewGroup)?.removeView(it)
        // Stop active loading and clear history.
        it.stopLoading()
        it.clearHistory()
        // Destroy the instance.
        it.destroy()
    }
    myWebView = null
    super.onDestroy()
}

جاوا

@Override
protected void onDestroy() {
    if (myWebView != null) {
        // Remove the WebView from its parent ViewGroup.
        if (myWebView.getParent() instanceof ViewGroup) {
            ((ViewGroup) myWebView.getParent()).removeView(myWebView);
        }
        // Stop active loading and clear history.
        myWebView.stopLoading();
        myWebView.clearHistory();
        // Destroy the instance.
        myWebView.destroy();
    }
    myWebView = null;
    super.onDestroy();
}

درک حافظه پس‌از تخریب

وقتی با destroy() تماس می‌گیرید، سیستم زمینه Activity را آزاد می‌کند، سلسله‌مراتب نما را پاک می‌کند، و کار پس‌زمینه وب را متوقف می‌کند. بااین‌حال، ممکن است متوجه شوید که حافظه فیزیکی فرایند (اندازه مجموعه مقیم) بلافاصله به خط پایه پیش‌از WebView کاهش نمی‌یابد.

این رفتار طبیعی است. تا زمانی که سیستم‌عامل حافظه نهان زمان اجرای بومی، کتابخانه‌های مشترک، و صفحات حافظه اختصاص‌داده‌شده را پس‌نگیرد یا فرایند خاتمه نیابد، این موارد در فرایند باقی می‌مانند. هدف اصلی destroy() جلوگیری از نشت حافظه Activity تجمعی هنگام پیمایش کاربران در داخل و خارج از صفحه‌های دارای پشتیبانی وب است.

سنجه‌های کلیدی اشکال‌زدایی

هنگام تجزیه‌وتحلیل مصرف حافظه WebView، روی سنجه‌های زیر تمرکز کنید:

  • اندازه مجموعه مقیم (RSS): کل RAM فیزیکی که در فرایند نگاشت شده است، ازجمله کد و کتابخانه‌های مشترک (در Android Studio Profiler به‌عنوان کل برچسب‌گذاری شده است).

  • ‫RSS ناشناس (RssAnon): حافظه تخصیص‌یافته مستقیماً توسط فرایندی که فایلی در دیسک از آن پشتیبانی نمی‌کند (مثل تخصیص‌های زمان اجرای جاوا اسکریپت و انبوهه بومی). این نشان‌دهنده هزینه حافظه اصلی محتوای وب شما است (در Android Studio Profiler به‌عنوان اختصاص‌داده‌شده برچسب‌گذاری شده است).

  • ردپای حافظه خصوصی (PMF): مجموع «مجموعه خدمات ناشناس» و مبادله (zRAM). ‫PMF نشان‌دهنده بار حافظه غیرقابل‌حذف واقعی است که برنامه شما بر سیستم تحمیل می‌کند.

  • «فایل PMF مرورگر» دربرابر «فایل PMF پردازنده»: حافظه استفاده‌شده توسط فرایند اصلی برنامه شما دربرابر حافظه استفاده‌شده توسط فرایند پردازنده مجزا. محتوای سنگین وب باعث ایجاد جهش‌های ناگهانی عمدتاً در فرایند تولیدکننده تصویر می‌شود.

  • تعداد اشیای زنده (WebViews، Activities، Views): تعداد نمونه‌های فعال «میانای کاربر»، «زمینه»، و WebView که در حافظه نگهداری می‌شوند. پیگیری این موارد مشخص می‌کند که آیا رشد حافظه ناشی از ارجاع‌های جاوا نگهداری‌شده است یا تخصیص‌های فقط بومی.

  • سایر خصوصی و پشته بومی: در dumpsys meminfo، تخصیص‌های بومی C/C++‎ و نگاشت‌های حافظه سفارشی (مثل Chromium PartitionAlloc یا پشته‌های زمان اجرای جاوا اسکریپت جاسازی‌شده) به‌جای «پشته جاوا» در «پشته بومی» و «سایر خصوصی» نشان داده می‌شود.

برای اطلاعات بیشتر درباره شمارنده‌های حافظه فرایند و دسته‌های آن‌ها، به واژه‌نامه حافظه فرایند مراجعه کنید.

گردش کار تشخیص خرابی عملی

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

ابزارهای نمایه‌سازی و تشخیص خرابی

برای بازرسی تخصیص‌های حافظه و تشخیص نشت، از ابزارهای زیر استفاده کنید:

  • تحلیل‌گر حافظه Android Studio: از تحلیل‌گر حافظه برای تصویرسازی تخصیص‌های بومی، ردیابی دسته‌های حافظه در طول زمان، و شناسایی Activity نشت‌ها در انتقال‌های صفحه استفاده کنید.

  • پیگیری حافظه با Perfetto: از Perfetto برای ضبط شمارنده‌های حافظه سطح سیستم (مثل RSS و RSS ناشناس) استفاده کنید تا رشد کلی حافظه را مشاهده کنید. توجه داشته باشید که تخصیص‌های موتور بومی WebView پشته‌های تماس را در ابزار نمایه‌سازی پشته Perfetto تولید نمی‌کند. برای بازرسی کردن نمای لحظه‌ای پشته جاوا اسکریپت و تخصیص‌های DOM در محتوای وب، از ابزارهای توسعه‌دهندگان Chrome استفاده کنید.

بررسی تعداد اشیای زنده

برای تعیین اینکه رشد حافظه ناشی از اشیای چارچوب Java نگهداری‌شده (مثل عناصر میانای کاربر) یا تخصیص‌های بومی است، Objects بخش dumpsys meminfo را بازرسی کنید:

adb shell dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects"

برونداد تعداد اشیای زنده را نمایش می‌دهد:

 Objects
               Views:     142         ViewRootImpl:        1
         AppContexts:       3           Activities:        1
              Assets:      12        AssetManagers:        0
       Local Binders:      32        Proxy Binders:       45
       Parcel memory:      15         Parcel count:       30
    Death Recipients:       2             WebViews:        1

این بخش تعداد اشیای چارچوب فعال، دسته‌های IPC، و تخصیص‌های Parcel را نمایش می‌دهد. برای تشخیص خرابی WebView، در درجه اول روی Activities و WebViews تمرکز کنید.

تعامل کاربر هدف (مثل باز و بسته کردن صفحه وب) را به‌طور مکرر انجام دهید و تعداد را مقایسه کنید:

  • نشت نمونه: اگر WebViews یا Activities در هر پیمایش افزایش می‌یابد و به خط پایه برنمی‌گردد، برنامه شما نمونه Java WebView یا میزبان Activity را نشت می‌دهد (برای مثال، به‌دلیل ViewGroup.removeView() یا ارجاع‌های شنونده نگهداری‌شده). چون یک Activity درختی از نمای کامل و منابع تصویر رمزگشایی‌شده را در حافظه سنجاق می‌کند، بازدیدهای مکرر به‌سرعت پشته Java را تمام می‌کند و باعث خرابی OutOfMemoryError می‌شود.

  • نشت بومی یا DOM: اگر WebViews و Activities ثابت بمانند درحالی‌که RSS کل فرایند و Private Other همچنان افزایش یابند، نشت از منابع بومی منتشرنشده، عناصر DOM، یا پیوندهای موتور JavaScript نشئت می‌گیرد. ازآنجایی‌که این تخصیص‌ها در حافظه بومی قرار دارند و از جمع‌آوری‌کننده زباله ART عبور می‌کنند، برای ابزارهای استاندارد تشخیص نشت Java نامرئی باقی می‌مانند و تا زمانی‌که سیستم‌عامل برنامه را متوقف کند، به انباشته شدن ادامه می‌دهند.

نمایه‌بندی فرایند رندرکننده مجزا بااستفاده از CLI

اجرای dumpsys meminfo با نام بسته برنامه شما فقط حافظه را برای فرایند میزبان اصلی برون‌برد می‌کند. برای بازرسی فرایند تولیدکننده تصویر مجزا که در آن صفحات وب پردازش می‌شوند:

  1. شناسه فرایند (PID) سرویس رندرکننده مجزا را پیدا کنید:

    adb shell dumpsys activity processes <var>PACKAGE_NAME</var> | grep "Isolated.*SandboxedProcessService"

    برونداد، گزارش فرایند مجزا و PID آن را نمایش می‌دهد RENDERER_PID (برای مثال، 22155):

    Isolated #5: ProcessRecord{... 22155:com.google.android.webview.debug:sandboxed_process0:...}
    
  2. بااستفاده از PID، خرابی حافظه فرایند رندرکننده را بررسی کنید:

    adb shell dumpsys meminfo <var>RENDERER_PID</var>
  3. فرایند برنامه میزبان را بازرسی کنید تا ردپای سمت مرورگر را ارزیابی کنید:

    adb shell dumpsys meminfo <var>PACKAGE_NAME</var>

بازرسی نقشه‌های حافظه و تخصیص‌ها

برای دیدن اینکه کدام زیرسیستم‌های بومی یا تخصیص‌دهنده‌ها حافظه ناشناس را اشغال می‌کنند، نقشه‌های حافظه فرایند را بازرسی کنید:

adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"

جدول زیر برچسب‌های حافظه ناشناس رایج و ارتباط آن‌ها را با رشد حافظه فهرست می‌کند:

برچسب حافظه زیرسیستم ارتباط با محتوای برنامه و وب دلیل رایج افزایش حافظه چیست؟
[anon:partition_alloc] ‫Chromium PartitionAlloc تخصیص‌های درخت‌های DOM، بافرهای پرداز، پشته V8 جاوا اسکریپت، و اجرای WebAssembly در WebView. بله (بالا): بار کردن صفحه‌های وب سنگین، DOMهای غنی از رسانه، یا فراخوانی ناموفق destroy() در نمونه‌های WebView دورریخته‌شده مستقیماً این برچسب را افزایش می‌دهد.
[anon:scudo...] یا [anon:libc_malloc] تخصیص‌دهنده‌های پشته بومی Android (Scudo / jemalloc) تخصیص‌های بومی عمومی C/C++ که توسط کتابخانه‌های NDK، پل‌های JNI، و خطوط لوله گرافیک بومی استفاده می‌شود. بله (متوسط تا زیاد): وقتی بسته‌بندی‌های JNI بومی یا وابستگی‌های C++ طرف سوم تخصیص‌های منتشرنشده را در سراسر پیمایش‌ها حفظ می‌کنند، رشد اتفاق می‌افتد.
‫[anon:...] (برای مثال، [anon:quickjs_heap...]) برنامه‌نویسی سفارشی یا زمان‌های اجرای بومی موتورهای جاوا اسکریپت جاسازی‌شده، زمان‌های اجرای WebAssembly سفارشی، یا استخرهای بافر بومی سفارشی. بله (وابسته به زمینه): در برنامه‌های ترکیبی که موتورهای دستورگان را در کنار نماهای بومی اجرا می‌کنند و نمی‌توانند پیوندهای زمان اجرا را پاک کنند رایج است.

محدودیت‌های میاناهای برنامه‌سازی کاربردی حافظه درون‌برنامه‌ای

«میاناهای برنامه‌سازی کاربردی حافظه درون‌برنامه‌ای» (مثل Debug.getMemoryInfo یا ActivityManager.getProcessMemoryInfo) فقط فرایند تماس را اندازه‌گیری می‌کنند. در حالت چندپردازشی، این «میاناهای برنامه‌سازی کاربردی» نمی‌توانند حافظه مصرف‌شده توسط فرایند رندرکننده مجزا را ضبط کنند. برای ارزیابی دقیق حافظه کل، به ابزارهای سیستم مثل dumpsys meminfo،‏ Perfetto، یا Android Studio Profiler تکیه کنید.

اولویت‌بندی حافظه بالا در برنامه ترکیبی

هنگام تشخیص رشد حافظه غیرقابل توضیح درطول WebView تعاملات تکرارشونده (مثل باز کردن پیوندهای وب یا پیمایش فیدهای مبتنی بر وب)، از گردش کار غربالگری زیر برای جدا کردن اینکه آیا نشت در لایه Java یا موتور بومی منشأ می‌گیرد استفاده کنید:

  1. نوع نشت را جدا کنید (جاوا دربرابر بومی): dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" را قبل و بعداز انتقال‌های مکرر کاربر (مثل باز و بسته کردن مقاله‌های وب یا تند کشیدن در فیدها) اجرا کنید.

    • مشاهده: اگر تعداد Activities و WebViews ثابت بماند (برای مثال، ۱ تا ۲ نمونه فعال)، برنامه Activity زمینه‌ها یا نمونه‌های WebView جاوا را فاش نمی‌کند.
  2. اندازه‌گیری دلتای حافظه در تعاملات (پیگیری سری زمانی): برای محاسبه نرخ تخصیص در هر انتقال، dumpsys meminfo نمای فوری در تعاملات کاربر مختلف ضبط کنید:

    • مشاهده: پشته Java در سطح حداکثر و سالم باقی می‌ماند (درطول استفاده افزایش می‌یابد و پس‌از جمع‌آوری زباله کاهش می‌یابد)، اما Private Other (دیگر خصوصی) و Native Heap (پشته بومی) به‌طور پیوسته در هر انتقال چند مگابایت افزایش می‌یابند. این نشان می‌دهد که نشت کاملاً در حافظه بومی خارج از زمان اجرای ART است. رونوشت‌های پشته جاوا استاندارد (.hprof) هیچ مشکلی را نشان نمی‌دهد.
  3. بازرسی نقشه‌های حافظه ناشناس: نقشه‌های حافظه فرایند را بااستفاده از ADB بررسی کنید (به بازرسی نقشه‌های حافظه و تخصیص‌ها مراجعه کنید):

    adb shell "cat /proc/$(pidof <var>PACKAGE_NAME</var>)/maps" | grep "anon:"
    • مشاهده: رشد حافظه در [anon:partition_alloc] یا پشته‌های موتور برنامه‌نویسی جاسازی‌شده متمرکز است و با افزایش تدریجی در ارجاع‌های سراسری JNI همراه است. این نشان می‌دهد که اگرچه نماهای Java جایگزین شده‌اند، اما اشیای صفحه بومی زیرین یا پیوندهای JavaScript آزاد نشده‌اند.
  4. اقدامات اصلاحی:

    • مطمئن شوید که هر WebView بازیافت‌شده یا دورریخته‌شده به‌طور صریح اسکریپت‌های فعال (stopLoading()) را متوقف می‌کند، سابقه را پاک می‌کند، و destroy() را فرا می‌خواند.
    • بازخوانی کردن کاربردهای پل جاوا اسکریپت سفارشی یا ارجاع‌های سراسری JNI مرتبط با نماهای بسته‌شده.
    • تأیید کنید که Private Other و پردازش RSS پس‌از انتقال‌های پیمایش پایدار می‌شود.

منابع بیشتر

برای کسب اطلاعات بیشتر درباره اشکال‌زدایی و نمایه‌سازی حافظه و عملکرد WebView، منابع زیر را ببینید: