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

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

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

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

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

  • اجرای چندفرایندی: در Android نسخه ۸.۰ (سطح میانای برنامه‌سازی کاربردی ۲۶) و بالاتر، WebView محتوای وب را از عملکردهای اصلی برنامه‌تان در چندین فرایند جدا می‌کند (در دستگاه‌های با حافظه دسترسی تصادفی کم، ممکن است به یک فرایند برگردد):

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

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

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

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

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

پاک‌سازی کردن نمونه‌های WebView

برای اطمینان از خاموش شدن تمیز و آزاد شدن منابع هنگام ازبین رفتن 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 را منتشر می‌کند، سلسله‌مراتب نما را پاک می‌کند، و کار پس‌زمینه وب را متوقف می‌کند. بااین‌حال، ممکن است متوجه شوید که حافظه فیزیکی فرایند (Resident Set Size) بلافاصله به خط مبنای پیش‌از WebView کاهش نمی‌یابد.

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

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

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

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

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

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

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

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

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

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

گردش‌کارهای تشخیص خرابی کاربردی

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

ابزارهای نمایه‌سازی و عیب‌یابی

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

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

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

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

برای تعیین اینکه آیا رشد حافظه ناشی از اشیاء چارچوب 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 کل فرایند و خصوصی دیگر همچنان افزایش یابد، نشت از منابع بومی منتشرنشده، عناصر DOM، یا پیوندهای موتور JavaScript منشأ می‌گیرد. چون این تخصیص‌ها در حافظه بومی قرار دارند و زباله‌روب ART را دور می‌زنند، برای ابزارهای استاندارد تشخیص نشت Java نامرئی می‌مانند و تا زمانی که سیستم‌عامل برنامه را متوقف کند، به انباشته شدن ادامه می‌دهند.

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

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

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

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

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

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

    • مشاهده: پشته Java محدود و سالم می‌ماند (درطول استفاده افزایش می‌یابد و پس‌از جمع‌آوری زباله کاهش می‌یابد)، اما خصوصی دیگر و پشته بومی به‌طور پیوسته چند مگابایت در هر انتقال افزایش می‌یابند. این ثابت می‌کند که نشت کاملاً در حافظه بومی خارج از زمان اجرای 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، منابع زیر را ببینید: