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

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

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

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

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

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

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

  • حجم الذاكرة الخاصة للمتصفّح مقابل حجم الذاكرة الخاصة للعارض: هي الذاكرة التي تستخدمها العملية الرئيسية لتطبيقك مقابل الذاكرة التي تستخدمها عملية العارض المعزولة. يؤدي محتوى الويب الكبير إلى ارتفاعات بشكلٍ أساسي في عملية العارض.

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

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

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

مهام سير عمل التشخيص العملية

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

أدوات تحديد المواصفات والتشخيص

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

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

  • تتبُّع الذاكرة باستخدام Perfetto: استخدِم Perfetto لتسجيل عدّادات الذاكرة على مستوى النظام (مثل حجم مجموعة البيانات الدائمة وحجم مجموعة البيانات الدائمة المجهولة) لمراقبة نمو الذاكرة بشكلٍ عام. يُرجى العِلم أنّ عمليات تخصيص المحرّك الأصلي في 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

يعرض هذا القسم أعداد كائنات إطار العمل النشطة ومقابض الاتصال بين العمليات وعمليات تخصيص Parcel. لتشخيص WebView، ركِّز بشكلٍ أساسي على Activities و WebViews.

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

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

  • تسرّب الذاكرة الأصلية أو نموذج كائن المستند (DOM): إذا بقيت أعداد WebViews وActivities ثابتة بينما يستمر إجمالي حجم مجموعة البيانات الدائمة للعملية وذاكرة أخرى خاصة في الارتفاع، فإنّ التسرّب ينشأ من الموارد الأصلية غير المحرَّرة أو عناصر نموذج كائن المستند (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. افحص تفاصيل ذاكرة عملية العارض باستخدام رقم تعريف العملية:

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

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

عند تشخيص نمو الذاكرة غير المبرَّر أثناء تفاعلات 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 وحجم مجموعة البيانات الدائمة للعملية بعد عمليات انتقال التنقّل.

مراجع إضافية

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