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

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

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

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

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

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

    • عملية المضيف (المتصفّح): هي عملية التطبيق الرئيسية التي يتم فيها تنفيذ Activity ورموز Java أو Kotlin.
    • عملية Isolated Renderer: هي عملية منفصلة في بيئة معزولة (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).

  • استهلاك الذاكرة الخاصة (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 وذاكرة 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، وعمليات تخصيص الحِزم. بالنسبة إلى بيانات تشخيص WebView، ركِّز بشكل أساسي على Activities وWebViews.

نفِّذ تفاعل المستخدم المستهدَف (مثل فتح شاشة ويب وإغلاقها) بشكل متكرّر وقارِن بين الأعداد:

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

  • تسريب الذاكرة الأصلية أو DOM: إذا ظلّت قيمتا WebViews وActivities ثابتتَين بينما يستمر ارتفاع إجمالي حجم RSS للعملية والذاكرة الخاصة الأخرى، يكون مصدر التسريب هو الموارد الأصلية غير المُحرَّرة أو عناصر DOM أو روابط محرك JavaScript. وبما أنّ عمليات التخصيص هذه تقع في الذاكرة الأصلية وتتجاوز أداة جمع البيانات غير المرغوب فيها في ART، تظل غير مرئية لأدوات رصد تسرب الذاكرة العادية في Java وتستمر في التراكم إلى أن يوقف نظام التشغيل التطبيق.

إنشاء ملف تعريف لعملية العارض المعزولة باستخدام واجهة سطر الأوامر

لا يؤدي تشغيل 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 المؤقتة لـ 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 أو استوديو Android Profiler.

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

عند تشخيص زيادة غير مفسّرة في استخدام الذاكرة أثناء عمليات التفاعل المتكرّرة 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 الأداء، اطّلِع على المراجع التالية: