مدیریت و تشخیص حافظه WebView

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

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

معماری حافظه WebView را درک کنید

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

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

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

  • تأثیر سیستمی حافظه بومی: برخلاف تخصیص‌های هیپ جاوا که توسط محدودیت maxHeap برنامه محدود می‌شوند و به سرعت با OutOfMemoryError از کار می‌افتند، حافظه بومی می‌تواند بی‌سروصدا تا گیگابایت افزایش یابد. از آنجایی که حافظه بومی آزاد نشده، رم فیزیکی و فضای swap (zRAM) را پر می‌کند، Low Memory Killer اندروید (LMK) شروع به خاتمه دادن به فرآیندهای پس‌زمینه برای بازیابی حافظه می‌کند. این امر قبل از از بین بردن نهایی برنامه پیش‌زمینه، عملکرد چندوظیفگی کلی دستگاه را کاهش می‌دهد.

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

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

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

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

معیارهای کلیدی اشکال‌زدایی

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

  • اندازه مجموعه مقیم (RSS): کل رم فیزیکی نگاشت شده به فرآیند، شامل کد و کتابخانه‌های مشترک (که در پروفایلر اندروید استودیو با عنوان Total مشخص شده است).

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

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

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

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

  • Private Other و Native Heap: در dumpsys meminfo ، تخصیص‌های بومی C/C++ و نگاشت‌های حافظه سفارشی (مانند Chromium PartitionAlloc یا هیپ‌های زمان اجرای جاوا اسکریپت تعبیه‌شده) به جای Java Heap، در Native Heap و Private Other ظاهر می‌شوند.

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

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

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

ابزارهای پروفایلینگ و تشخیصی

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

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

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

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

برای تعیین اینکه آیا رشد حافظه ناشی از کُند شدن wrapper های جاوا یا تخصیص‌های بومی است، بخش 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

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

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

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

فرآیند رندر ایزوله را با استفاده از CLI پروفایل کنید

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

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

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

    خروجی، رکورد فرآیند ایزوله شده و 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] تخصیص پارتیشن کرومیوم تخصیص منابع برای درخت‌های DOM، بافرهای رندر، هیپ جاوا اسکریپت V8 و اجرای WebAssembly در WebView . بله (زیاد): بارگذاری صفحات وب سنگین، DOM های غنی از رسانه یا عدم فراخوانی destroy() در نمونه‌های WebView حذف شده، مستقیماً این تگ را افزایش می‌دهد.
[anon:scudo...] یا [anon:libc_malloc] تخصیص‌دهنده‌های هیپ بومی اندروید ( Scudo / jemalloc) تخصیص‌های عمومی بومی C/C++ که توسط کتابخانه‌های NDK، پل‌های JNI و خطوط لوله گرافیکی بومی استفاده می‌شوند. بله (متوسط ​​تا زیاد): رشد زمانی رخ می‌دهد که JNI wrapper های بومی یا وابستگی‌های شخص ثالث C++، تخصیص‌های منتشر نشده را در سراسر ناوبری‌ها حفظ کنند.
[anon:...] (برای مثال، [anon:quickjs_heap...] ) اسکریپت‌نویسی سفارشی یا زمان‌های اجرای بومی موتورهای جاوا اسکریپت تعبیه‌شده، زمان‌های اجرای سفارشی WebAssembly یا مجموعه‌های بافر بومی سفارشی. بله (وابسته به متن): در برنامه‌های ترکیبی که موتورهای اسکریپت‌نویسی را در کنار نماهای بومی اجرا می‌کنند و در پاکسازی اتصالات زمان اجرا ناموفق هستند، رایج است.

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

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

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

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

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

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

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

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

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

منابع اضافی

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

،

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

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

معماری حافظه WebView را درک کنید

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

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

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

  • تأثیر سیستمی حافظه بومی: برخلاف تخصیص‌های هیپ جاوا که توسط محدودیت maxHeap برنامه محدود می‌شوند و به سرعت با OutOfMemoryError از کار می‌افتند، حافظه بومی می‌تواند بی‌سروصدا تا گیگابایت افزایش یابد. از آنجایی که حافظه بومی آزاد نشده، رم فیزیکی و فضای swap (zRAM) را پر می‌کند، Low Memory Killer اندروید (LMK) شروع به خاتمه دادن به فرآیندهای پس‌زمینه برای بازیابی حافظه می‌کند. این امر قبل از از بین بردن نهایی برنامه پیش‌زمینه، عملکرد چندوظیفگی کلی دستگاه را کاهش می‌دهد.

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

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

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

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

معیارهای کلیدی اشکال‌زدایی

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

  • اندازه مجموعه مقیم (RSS): کل رم فیزیکی نگاشت شده به فرآیند، شامل کد و کتابخانه‌های مشترک (که در پروفایلر اندروید استودیو با عنوان Total مشخص شده است).

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

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

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

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

  • Private Other و Native Heap: در dumpsys meminfo ، تخصیص‌های بومی C/C++ و نگاشت‌های حافظه سفارشی (مانند Chromium PartitionAlloc یا هیپ‌های زمان اجرای جاوا اسکریپت تعبیه‌شده) به جای Java Heap، در Native Heap و Private Other ظاهر می‌شوند.

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

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

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

ابزارهای پروفایلینگ و تشخیصی

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

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

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

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

برای تعیین اینکه آیا رشد حافظه ناشی از کُند شدن wrapper های جاوا یا تخصیص‌های بومی است، بخش 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

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

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

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

فرآیند رندر ایزوله را با استفاده از CLI پروفایل کنید

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

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

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

    خروجی، رکورد فرآیند ایزوله شده و 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] تخصیص پارتیشن کرومیوم تخصیص منابع برای درخت‌های DOM، بافرهای رندر، هیپ جاوا اسکریپت V8 و اجرای WebAssembly در WebView . بله (زیاد): بارگذاری صفحات وب سنگین، DOM های غنی از رسانه یا عدم فراخوانی destroy() در نمونه‌های WebView حذف شده، مستقیماً این تگ را افزایش می‌دهد.
[anon:scudo...] یا [anon:libc_malloc] تخصیص‌دهنده‌های هیپ بومی اندروید ( Scudo / jemalloc) تخصیص‌های عمومی بومی C/C++ که توسط کتابخانه‌های NDK، پل‌های JNI و خطوط لوله گرافیکی بومی استفاده می‌شوند. بله (متوسط ​​تا زیاد): رشد زمانی رخ می‌دهد که JNI wrapper های بومی یا وابستگی‌های شخص ثالث C++، تخصیص‌های منتشر نشده را در سراسر ناوبری‌ها حفظ کنند.
[anon:...] (برای مثال، [anon:quickjs_heap...] ) اسکریپت‌نویسی سفارشی یا زمان‌های اجرای بومی موتورهای جاوا اسکریپت تعبیه‌شده، زمان‌های اجرای سفارشی WebAssembly یا مجموعه‌های بافر بومی سفارشی. بله (وابسته به متن): در برنامه‌های ترکیبی که موتورهای اسکریپت‌نویسی را در کنار نماهای بومی اجرا می‌کنند و در پاکسازی اتصالات زمان اجرا ناموفق هستند، رایج است.

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

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

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

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

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

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

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

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

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

منابع اضافی

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