تسرّبات الذاكرة وإصلاحها
اكتشف التسرّبات وأوقفها
تسرّبات الذاكرة وإصلاحها درس مجاني في Android Academy على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Android Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Android Academy 4 دروس في المجموع.
ما تسريب الذاكرة؟
يحدث تسريب الذاكرة عندما يتعذر جمع الكائنات التي لم تعد مطلوبة بواسطة جامع البيانات المهملة، لأن شيئًا ما لا يزال يحتفظ بمرجع إليها. وفي Android، تكون الضحية الكلاسيكية هي Activity أو Context التي تبقى بعد تدمير شاشتها.
تزيد التسريبات حجم الكومة بمرور الوقت، وتؤدي إلى عمليات أكثر تكرارًا لجمع البيانات المهملة (فتوقف واجهة المستخدم وتتسبب في التقطّع)، ثم تتسبب في النهاية في تعطل التطبيق بالخطأ OutOfMemoryError. يوضح هذا الدرس كيفية العثور عليها وإصلاحها.
كيف يقرر GC ما الذي يحتفظ به
يحتفظ جامع البيانات المهملة في Android بأي كائن يمكن الوصول إليه من جذر GC (مثل مؤشرات الترابط النشطة والحقول الساكنة وغيرها). أما كل ما يتعذر الوصول إليه فيُحرر.
التسريب هو ببساطة مسار وصول غير مرغوب فيه: كائن طويل العمر يحتفظ بمرجع إلى كائن قصير العمر. يوضح العرض التوضيحي النقي بلغة Kotlin أدناه كيف يؤدي الوصول إلى إبقاء كائن حيًا.
object GlobalCache {
val items = mutableListOf<ByteArray>()
}
fun cacheSomething() {
// This 1MB array stays alive forever because GlobalCache
// (a static singleton, a GC root) keeps referencing it.
GlobalCache.items.add(ByteArray(1_000_000))
}
fun main() {
repeat(3) { cacheSomething() }
println("Held arrays: ${GlobalCache.items.size}") // never freed
}الكلاسيكية: تسرّب Activity
أكثر حالات تسرّب الذاكرة شيوعًا في Android هي احتفاظ كائن static أو طويل العمر بـ Context. عند تدمير Activity (بسبب تدوير الشاشة أو الانتقال)، يريد النظام تحريرها، لكن المرجع static يبقيها حيّة إلى الأبد.
تسبّب الشيفرة أدناه تسرّبًا: إذ يخزّن singleton سياق Activity مؤقتًا.
// LEAK: static field holds an Activity Context
object Analytics {
var context: Context? = null // <- keeps Activity alive
fun init(ctx: Context) { context = ctx }
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.init(this) // passing the Activity -> leak on rotation
}
}الإصلاح: استخدام Application Context
عندما تحتاج إلى تخزين Context داخل شيء طويل العمر، خزّن سياق التطبيق، فهو يعيش طوال مدة العملية ومن الآمن الاحتفاظ به.
احتفظ بسياق Activity/View فقط طوال مدة وجود تلك الشاشة. تمنع هذه القاعدة وحدها معظم حالات التسرّب في Android.
object Analytics {
private var appContext: Context? = null
fun init(ctx: Context) {
// applicationContext is process-scoped and safe to keep
appContext = ctx.applicationContext
}
}
// Call site is now leak-free:
Analytics.init(this) // stores applicationContext, not the Activityالفئات الداخلية و Handlers
يحتفظ الصف الداخلي غير static (بما في ذلك معظم المستمعات وRunnables وعمليات الاستدعاء في Handler) بمرجع ضمني إلى الصف الخارجي. إذا استمر هذا العمل بعد انتهاء عمر الشاشة، فإنه يسرّب الشاشة.
يُعد Handler.postDelayed المؤجل سببًا شائعًا لذلك: إذ تبقي الرسالة المعلّقة Activity حيّة إلى أن تُنفّذ.
// LEAK: the posted Runnable holds the Activity for 60 seconds
handler.postDelayed({ updateUi() }, 60_000)
// FIX: cancel pending work when the screen goes away
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}Coroutines: تحديد نطاق العمل
تسبّب coroutine التي يستمر عمرها بعد انتهاء عمر الشاشة تسرّب كل ما تلتقطه. والإصلاح هو التزامن المنظّم: شغّل العمل ضمن نطاق مرتبط بدورة حياة، بحيث يُلغى تلقائيًا.
استخدم viewModelScope في ViewModel، وlifecycleScope في متحكم واجهة المستخدم. عند تدمير المالك، يُلغي النطاق عمله وتُحرَّر المراجع.
class FeedViewModel : ViewModel() {
fun load() {
// Cancelled automatically when the ViewModel is cleared
viewModelScope.launch {
val feed = repository.fetchFeed()
_state.value = feed
}
}
}
// In Compose, collect tied to the lifecycle:
val state by viewModel.state.collectAsStateWithLifecycle()DisposableEffect في Compose
في Compose، يجب إلغاء تسجيل أي شيء تقوم بتسجيله عند مغادرة composable. يوفّر DisposableEffect كتلة onDispose لهذا الغرض تحديدًا: للمستمعات والمراقبين والمستشعرات ومستقبلات البث.
يؤدي نسيان التنظيف هنا إلى تسرّب callback وكل ما تلتقطه.
@Composable
fun LocationDisplay(manager: LocationManager) {
DisposableEffect(manager) {
val listener = LocationListener { /* update */ }
manager.register(listener)
onDispose { manager.unregister(listener) } // prevents the leak
}
}اكتشاف التسرّبات باستخدام LeakCanary
تُعد LeakCanary الأداة القياسية لاكتشاف التسرّبات تلقائيًا في إصدارات التصحيح. أضف الاعتمادية، وستراقب الكائنات المدمّرة؛ وإذا لم يُجمع أحدها بواسطة جامع البيانات المهملة، فستفرّغ الذاكرة المؤقتة وتعرض لك سلسلة المراجع الدقيقة التي تبقيه حيًا.
توضح لك هذه السلسلة كيفية الإصلاح: فهي تشير مباشرةً إلى الحقل أو المستمع المتسبب في المشكلة.
// build.gradle.kts (app module)
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// No code needed: on a debug build, navigate away from a screen and
// LeakCanary posts a notification with the leak trace, e.g.:
// Analytics.context -> MainActivity (leaked)تفريغ الذاكرة المؤقتة في Profiler
بالنسبة إلى التسرّبات التي لا ترصدها LeakCanary (مثل النمو البطيء والتسرّبات الأصلية وذاكرات التخزين المؤقت الكبيرة)، استخدم Memory Profiler. التقط تفريغًا للذاكرة المؤقتة، وفرض تشغيل GC، وافحص الفئات التي لديها عدد مثيلات أكبر من المتوقع.
يُعد ازدياد عدد مثيلات Activity أو Fragment أو ViewModel بعد الانتقال بعيدًا عنها دليلًا قاطعًا على وجود مشكلة. يعرض Profiler أيضًا المسار إلى جذور GC، كي تتمكن من تتبع المرجع.
الصور النقطية والتخصيصات الكبيرة
ليست كل مشكلة في الذاكرة تسرّبًا للمراجع. فقد تستهلك الصور النقطية وذاكرات التخزين المؤقت غير المحدودة مساحة heap بالكامل بمفردها. وقد تشغل صورة كاملة الدقة عشرات الميغابايتات في الذاكرة.
دع مكتبة صور (Coil/Glide) تخفّض العينة إلى الحجم المعروض، وحدّد حجم أي ذاكرة تخزين مؤقت تنشئها بحد أقصى صريح. يوضح المقتطف ذاكرة تخزين مؤقت LRU محدودة الحجم.
// Bound memory: keep at most ~1/8 of available app memory
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val bitmapCache = object : LruCache<String, Bitmap>(maxKb) {
override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// An unbounded HashMap<String, Bitmap> would grow until OutOfMemoryError.قائمة التحقق لمنع التسرّبات
اكتسب هذه العادات، ولن تحدث معظم التسرّبات من الأساس:
- خزّن applicationContext في الكائنات طويلة العمر، ولا تخزّن Activity/View مطلقًا.
- شغّل العمل غير المتزامن في coroutine محددة بنطاق دورة الحياة (
viewModelScope،lifecycleScope). - احرص دائمًا على إلغاء تسجيل المستمعات/المستقبلات (
onDispose،onDestroy). - ألغِ Handlers المؤجلة والمؤقتات.
- حدّد حجم ذاكرات التخزين المؤقت وخفّض عينات الصور النقطية.
- أبقِ LeakCanary في إصدارات التصحيح، وتعامل مع مسارات التسرّب التي تعرضها.
اختبار سريع
تحتاج إلى كائن singleton للاحتفاظ بـ Context طوال مدة حياته. ما السياق الذي يجب أن يحتفظ به لتجنب تسرّب الشاشة؟
مراجعة: الحفاظ على نظافة الذاكرة
تعلّمت ما هي التسرّبات وكيفية إيقافها:
- التسرّبات هي مسارات إمكانية الوصول غير المرغوب فيها التي تبقي الكائنات الميتة حيّة.
- التسرّب الكلاسيكي هو مرجع static أو طويل العمر إلى Activity/Context — استخدم applicationContext بدلًا منه.
- تسرّب مستمعات الفئات الداخلية وHandlers المؤجلة وcoroutines غير المحددة النطاق مالكها — حدّد نطاقها وألغها.
- استخدم
DisposableEffectفي Compose لإلغاء تسجيل عمليات الاستدعاء. - تكشف LeakCanary وMemory Profiler سلسلة المراجع.
- حدّد حجم ذاكرات التخزين المؤقت وخفّض عينات الصور النقطية.
التالي: جعل التطبيق يبدأ بسرعة باستخدام تحسين بدء التشغيل وملفات التعريف الأساسية.
الأسئلة الشائعة
هل درس «تسرّبات الذاكرة وإصلاحها» مجاني؟
نعم — نص درس «تسرّبات الذاكرة وإصلاحها» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Android Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Android Academy 4 دروس في المجموع.
ماذا ستتعلم في «تسرّبات الذاكرة وإصلاحها»؟
اكتشف التسرّبات وأوقفها تتمرن على Android Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Android Academy؟
لا تُشترط خبرة سابقة. Android Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.
كم من الوقت يستغرق درس «تسرّبات الذاكرة وإصلاحها»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Android Academy هذا؟
نعم. كل درس في Android Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- قياس الأداء
- السيطرة على إعادة التركيب
- تسرّبات الذاكرة وإصلاحها
- ملفات تعريف بدء التشغيل والأساس