إدارة ذاكرة WebView وتشخيصها

يُشغِّل WebView رموزًا برمجية أصلية في عمليات متعددة لعرض المحتوى على الويب في تطبيق Android. وقد يؤدي ترك نُسخ WebView بدون إدارة إلى تسرُّب الذاكرة وتعطُّل التطبيق بسبب نفاد الذاكرة (OOM) وتراجُع أداء التطبيق.

يشرح هذا المستند نموذج الذاكرة المتعدد العمليات في WebView، ويصف كيفية إدارة دورة حياته بشكل صحيح لمنع تسرُّب الذاكرة، ويقدّم مهام سير عمل عملية لتشخيص مشاكل الذاكرة.

فهم بنية ذاكرة WebView

لإدارة ذاكرة WebView بفعالية، يجب فهم كيفية تخصيص Android للموارد لمحتوى الويب:

  • التنفيذ المتعدّد العمليات: في الإصدار 8.0 من نظام التشغيل Android (المستوى 26 لواجهة برمجة التطبيقات) والإصدارات الأحدث، يفصل WebView المحتوى على الويب عن الوظائف الأساسية لتطبيقك من خلال عمليات متعدّدة (على الأجهزة ذات ذاكرة الوصول العشوائي المنخفضة، قد يعود إلى عملية واحدة):

    • عملية المضيف (المتصفّح): هي عملية التطبيق الرئيسية التي يتم فيها تشغيل رمز Activity ورمز Java أو Kotlin.
    • عملية عرض معزولة: هي عملية منفصلة في وضع الحماية (SandboxedProcessService) تعمل على تحليل HTML وCSS وتنفيذ JavaScript وعرض صفحات الويب.
  • استهلاك الذاكرة الأصلية: يتم تخصيص معظم ذاكرة WebView، بما في ذلك الرسومات المعروضة وشجرة نموذج العناصر في المستند وذاكرة وقت تشغيل JavaScript، في الذاكرة الأصلية، وليس في كومة ذاكرة Java. لا تعرض لقطة أجزاء من الذاكرة في Java (.hprof) سوى عنصر تضمين Java خفيف الوزن ولا تلتقط الذاكرة الحقيقية التي يستخدمها المحتوى على الويب.

  • تأثير الذاكرة الأصلية في النظام: على عكس عمليات تخصيص الذاكرة المؤقتة في Java، والتي تكون محدودة بحدّ maxHeap الأقصى للتطبيق وتتعطّل بسرعة مع ظهور خطأ OutOfMemoryError، يمكن أن تزداد الذاكرة الأصلية تلقائيًا إلى غيغابايت. عندما تمتلئ ذاكرة الوصول العشوائي (RAM) ومساحة الإبدال (zRAM) بالذاكرة الأصلية غير المحرَّرة، تبدأ ميزة Low Memory Killer (LMK) في Android بإنهاء العمليات في الخلفية لإخلاء مساحة في الذاكرة. ويؤدي ذلك إلى تدهور أداء الجهاز بشكل عام في ما يتعلق بتعدد المهام قبل أن يتم في النهاية إيقاف التطبيق الذي يعمل في المقدّمة.

إدارة مراحل نشاط WebView

تُعدّ الإدارة الصحيحة لمراحل النشاط أمرًا بالغ الأهمية لمنع تسرُّب الذاكرة. من الأخطاء الشائعة افتراض أنّ إزالة WebView من التنسيق أو السماح لـ Activity بالانتهاء يؤدي تلقائيًا إلى إخلاء الذاكرة.

لضمان حذف كل من مراجع سياق Java وموارد العرض الأصلية بالكامل، يجب تنظيم تسلسل الإيقاف بشكل صريح في مراحل نشاط المكوّن المضيف (مثل onDestroy())، وإيقاف تنفيذ الصفحة النشطة، وفصل العرض عن الحاوية، وإلغاء عمليات الربط الأصلية.

إزالة مثيلات WebView

لضمان إيقاف التشغيل بشكل سليم وتحرير الموارد عند تدمير Activity أو Fragment، اتّبِع الخطوات التالية:

  1. أزِل WebView من الحاوية الرئيسية (ViewGroup).
  2. إيقاف التحميل النشط ومحو سجلّ التنقّل
  3. الاتصال بالرقم destroy().
  4. محو المرجع إلى null

يوضّح المثال التالي كيفية تنظيف WebView بشكل صحيح:

Kotlin

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()
}

Java

@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").

  • مجموعة البيانات الدائمة المجهولة (RssAnon): هي الذاكرة التي تخصصها العملية مباشرةً والتي لا يتم الاحتفاظ بنسخة احتياطية منها في ملف على القرص (مثل كومة الذاكرة المؤقتة الأصلية وتخصيصات وقت تشغيل JavaScript). يمثّل ذلك تكلفة الذاكرة الأساسية للمحتوى على الويب (المصنّفة على أنّها مخصّصة في استوديو Android Profiler).

  • استهلاك الذاكرة الخاصة (PMF): مجموع ذاكرة RSS المجهولة ومساحة الإبدال (zRAM) يعكس مقياس PMF عبء الذاكرة الفعلي غير القابل للإخلاء الذي يفرضه تطبيقك على النظام.

  • مقارنة بين PMF للمتصفّح وPMF للعرض: الذاكرة التي تستخدمها العملية الرئيسية لتطبيقك مقارنةً بالذاكرة التي تستخدمها عملية العرض المعزولة. يتسبب المحتوى الثقيل على الويب في حدوث ارتفاعات في عملية العرض بشكل أساسي.

  • عدد العناصر النشطة (WebViews وActivities وViews): عدد واجهات المستخدم النشطة والسياق ومثيلات WebView المحفوظة في الذاكرة. ويساعد تتبُّع هذه المراجع في تحديد ما إذا كان تزايد استخدام الذاكرة ناتجًا عن مراجع Java المحتجَزة أو عمليات التخصيص الأصلية فقط.

  • الذاكرة الخاصة الأخرى والذاكرة الديناميكية الأصلية: في الإصدار dumpsys meminfo، تظهر عمليات تخصيص الذاكرة الأصلية بلغة C/C++‎ وعمليات ربط الذاكرة المخصّصة (مثل PartitionAlloc في Chromium أو الذاكرة الديناميكية المضمّنة لوقت تشغيل JavaScript) ضمن "الذاكرة الديناميكية الأصلية" و"الذاكرة الخاصة الأخرى" بدلاً من "الذاكرة الديناميكية في Java".

لمزيد من المعلومات حول عدّادات ذاكرة العمليات وفئاتها، يُرجى الاطّلاع على مسرد ذاكرة العمليات.

سير عمل تشخيصي عملي

بما أنّ WebView يعمل عبر عمليات متعددة ويخصّص ذاكرة أصلية، يمكنك استخدام الأدوات والأساليب التالية لفحص أثره:

أدوات تحليل الأداء والتشخيص

لفحص عمليات تخصيص الذاكرة وتشخيص حالات التسرُّب، استخدِم الأدوات التالية:

  • محلّل الذاكرة في "استوديو Android": يمكنك استخدام محلّل الذاكرة لعرض عمليات التخصيص الأصلية وتتبُّع فئات الذاكرة بمرور الوقت ورصد Activity أي تسرُّب للبيانات أثناء الانتقال بين الشاشات.

  • تتبُّع الذاكرة باستخدام Perfetto: استخدِم Perfetto لتسجيل عدّادات الذاكرة على مستوى النظام (مثل RSS وAnonymous RSS) لمراقبة النمو الإجمالي للذاكرة. يُرجى العِلم أنّ عمليات تخصيص المحرّك الأصلي في WebView لا تنتج عنها سلاسل استدعاء في أداة تحليل لقطات العناصر المتعدّدة في Perfetto. استخدِم أدوات مطوّري البرامج في Chrome لفحص لقطات الذاكرة المتعدّدة في JavaScript وتخصيصات نموذج العناصر في المستند (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 JavaScript المؤقتة وتنفيذ WebAssembly في "WebView" نعم (مرتفع): يؤدي تحميل صفحات ويب ثقيلة أو نماذج DOM غنية بالوسائط أو عدم استدعاء destroy() في نُسخ WebView التي تم تجاهلها إلى تضخيم هذه العلامة مباشرةً.
[anon:scudo...] أو [anon:libc_malloc] أدوات تخصيص الذاكرة المؤقتة الأصلية في Android (Scudo / jemalloc) عمليات التخصيص العامة لـ C/C++‎ التي تستخدمها مكتبات NDK وجسور JNI ومسارات الرسومات الأصلية نعم (متوسط إلى مرتفع): يحدث النمو عندما تحتفظ برامج تضمين JNI الأصلية أو تبعيات C++ التابعة لجهات خارجية بعمليات تخصيص غير منشورة عبر عمليات التنقّل.
[anon:...] (مثلاً، [anon:quickjs_heap...]) النصوص البرمجية المخصّصة أو بيئات التشغيل الأصلية محرّكات JavaScript المضمّنة أو بيئات تشغيل WebAssembly المخصّصة أو مجموعات المخازن المؤقتة الأصلية المخصّصة نعم (حسب السياق): يشيع ذلك في التطبيقات المختلطة التي تنفّذ محرّكات النصوص البرمجية إلى جانب طرق العرض الأصلية ولا تنجح في تنظيف عمليات الربط في وقت التشغيل.

القيود المفروضة على واجهات برمجة التطبيقات الخاصة بالذاكرة داخل التطبيق

لا تقيس واجهات برمجة التطبيقات الخاصة بالذاكرة داخل التطبيق (مثل Debug.getMemoryInfo أو ActivityManager.getProcessMemoryInfo) سوى عملية طلب البيانات. في وضع العمليات المتعدّدة، لا يمكن لواجهات برمجة التطبيقات هذه تسجيل الذاكرة التي تستهلكها عملية العرض المعزولة. لتقييم إجمالي الذاكرة بدقة، يمكنك الاعتماد على أدوات النظام، مثل dumpsys meminfo أو Perfetto أو &quot;محلّل الأداء في استوديو Android&quot;.

تحديد أولويات الاستخدام العالي للذاكرة في تطبيق مختلط

عند تشخيص زيادة غير مبرَّرة في استخدام الذاكرة أثناء تفاعلات WebView المتكررة (مثل فتح روابط الويب أو التنقّل في الخلاصات المستندة إلى الويب)، يُرجى استخدام سير العمل التالي لتحديد ما إذا كان التسريب ناتجًا عن طبقة Java أو المحرّك الأصلي:

  1. تحديد نوع التسرُّب (Java مقابل الرمز البرمجي الأصلي): شغِّل dumpsys meminfo -a <var>PACKAGE_NAME</var> | grep -A 10 "Objects" قبل وبعد عمليات انتقال المستخدم المتكررة (مثل فتح وإغلاق مقالات الويب أو التمرير السريع عبر الخلاصات).

    • ملاحظة: إذا ظلّت أعداد Activities وWebViews ثابتة (على سبيل المثال، مثيل واحد أو مثيلان نشطان)، فهذا يعني أنّ التطبيق لا يسرّب سياقات Activity أو مثيلات WebView في Java.
  2. قياس الفرق في الذاكرة على مستوى التفاعلات (تتبُّع السلسلة الزمنية): يمكنك التقاط لقطات dumpsys meminfo على مستوى تفاعلات متعددة للمستخدمين لاحتساب معدّل التخصيص لكل عملية انتقال:

    • الملاحظة: تظلّ كومة ذاكرة Java محدودة وسليمة (ترتفع أثناء الاستخدام وتنخفض بعد تجميع المهملات)، ولكن الخاصة الأخرى والكومة الأصلية ترتفعان بشكل مطّرد بمقدار عدة ميغابايت لكل عملية انتقال. ويثبت ذلك أنّ التسرُّب يحدث بالكامل في الذاكرة الأصلية خارج بيئة التشغيل ART. لن تظهر أي مشاكل في لقطات أجزاء من الذاكرة العادية في Java (.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().
    • إزالة عمليات الاستدعاء المخصّصة لربط JavaScript أو مراجع JNI العامة المرتبطة بالعروض التي تم إغلاقها
    • تأكَّد من استقرار Private Other وRSS للعملية بعد عمليات الانتقال في التنقّل.

مراجع إضافية

لمزيد من المعلومات حول تصحيح الأخطاء وتحليل أداء الذاكرة وWebView، يُرجى الاطّلاع على المراجع التالية: