تسرب الذاكرة — واحدة من أكثر المشاكل غدراً في تطوير التطبيقات المحمولة. استخدام التطبيق للذاكرة ينمو باستمرار حتى يصل إلى الحد الذي يحدده نظام التشغيل، يتبعه OutOfMemoryError أو إنهاء إجباري. وفقاً لـ Square Engineering، حوالي 40% من تطبيقات Android لديها على الأقل تسرب ذاكرة واحد لا يمكن اكتشافه إلا من خلال التنميط. دعنا نستعرض الأسباب وطرق منع نمو الذاكرة.
الخلاصة
تسرب الذاكرة — حالة يستمر فيها الاحتفاظ بكائن لم يعد التطبيق بحاجته في heap لأن مرجعاً نشطاً من المجموعة الجذرية (GC Root) لا يزال يشير إليه. يعتبر جامع القمامة مثل هذا الكائن حياً ولا يزيله.
انتفاخ الذاكرة — مشكلة أوسع حيث يستهلك التطبيق ذاكرة أكثر من اللازم لأداء مهامه الحالية. الأسباب: التخزين المؤقت المفرط، تكرار الكائنات، هياكل البيانات غير المثلى، وتجزئة heap.
في Android، يتم تخصيص heap محدود لكل تطبيق (عادة 64–512 ميغابايت حسب الجهاز وإصدار نظام التشغيل). في iOS، الحد أقل صرامة، لكن النظام يرسل تحذيراً عند الاقتراب من الحد.
| الخاصية | Android | iOS |
|---|---|---|
| حد heap | 64–512 ميغابايت (حسب الجهاز) | ضمني (نظام) |
| جمع القمامة | ART (متزامن، مضغوط) | ARC (العد التلقائي للمراجع) |
| آلية التسرب | مراجع GC Root | دورات الاحتفاظ (دورات مرجعية قوية) |
| النتيجة | OutOfMemoryError | تحذير ذاكرة → إنهاء |
وفقاً لـ Facebook Engineering Blog، تتسبب تسربات الذاكرة في ~15% من تقارير الأعطال في التطبيقات المحمولة. في Android، يضاف إلى ذلك ANR بسبب توقفات GC المتكررة عند انخفاض الذاكرة.
مرجع ثابت إلى Activity — تسرب كلاسيكي في Android. إذا كان حقل ثابت أو singleton يحتفظ بمرجع إلى Activity، فلن يتم جمعها بواسطة GC حتى بعد finish() ما دام singleton حياً. Activity هي كائن ثقيل يحتوي على تسلسل هرمي للعرض وموارد و Context.
object LeakHolder {
var activityRef: Activity ?= null // leak: static reference to Activity
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
LeakHolder.activityRef = this // ❌ MainActivity will never be GC'd
}
}
الفئات المجهولة واللامبدات — تحتفظ ضمنياً بمرجع إلى الفئة الخارجية. إذا تم تمرير Runnable أو Callback إلى خدمة خارجية وتم تدمير Activity، فإن كائن الفئة المجهولة لا يزال في قائمة الانتظار ويمنع Activity من جمع القمامة.
في iOS، المشكلة الرئيسية هي دورات الاحتفاظ: كائنان يحتفظان بمراجع قوية لبعضهما البعض، ولا يستطيع ARC تصفير عداد المراجع لأي منهما. حالة نموذجية: closure يلتقط self بقوة، و self يحتفظ بمرجع إلى closure.
LeakCanary — مكتبة من Square للكشف التلقائي عن التسربات في Android. بعد تدمير Activity أو Fragment، تتحقق مما إذا كان الكائن قد تم جمعه بواسطة GC. إذا لم يكن كذلك، تقوم بعمل heap dump وتظهر تتبع التسرب.
// LeakCanary 2.x — auto-integration via Application
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary auto-installs in debug build
// via ContentProvider — zero code setup
}
}
// Force check invocation
AppWatcher.objectWatcher.watch(watchedObject, "leak description")
Android Studio Profiler — أداة مدمجة لمراقبة الذاكرة في الوقت الفعلي. تسمح بتسجيل heap dump، والعثور على كائنات مشبوهة (Retained Size > 1 ميغابايت)، وتتبع مسار GC root إلى كل كائن.
لـ iOS، استخدم Xcode Memory Graph Debugger. يقوم بتصور رسم بياني للكائنات في الذاكرة، ويظهر دورات الاحتفاظ ويسمح باكتشاف المراجع الدائرية فوراً. Instruments > Allocations متاح أيضاً للمراقبة طويلة المدى.
WeakReference — آلية أساسية للمراجع التي لا يجب أن تتداخل مع جمع القمامة. إذا قرر GC جمع كائن، فإن WeakReference يعيد null. يُستخدم للمعاودات والمستمعين والمراجع لمكونات واجهة المستخدم من خيوط الخلفية.
مكونات دورة الحياة — نهج معماري مطبق في Android Jetpack (Lifecycle، LiveData، Flow، coroutines). يتم إلغاء الاشتراكات تلقائياً عند onDestroy، مما يلغي الفئة الرئيسية من التسربات.
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<List<User>>()
val data: LiveData<List<User>> get() = _data
fun loadData() {
viewModelScope.launch {
val result = repository.fetchData()
_data.postValue(result)
// coroutine auto-cancels on onCleared()
}
}
}
viewModelScope و lifecycleScope — CoroutineScope مدمجان في Android يتم إلغاؤهما عند حدث دورة الحياة المقابل. هذا يلغي التسربات عبر coroutines — السيناريو الأكثر شيوعاً في تطوير Android الحديث.
Memory Profiler في Android Studio — الأداة الأساسية لمراقبة heap. يعرض التخصيصات المباشرة ولقطات heap وعدد الكائنات حسب النوع. يسمح بتسجيل تفريغ وتحليله في MAT (Memory Analyzer Tool) للعثور على كائنات مشبوهة.
Eclipse MAT — محلل سطح مكتب لـ heap dump. بعد تحميل ملف HPROF من Android Studio، يبني MAT شجرة مسيطرة، ويظهر retain size لكل كائن، ويقدم تحليلاً تلقائياً للتسربات المشبوهة عبر Leak Suspects Report.
Xcode Memory Graph — مصحح بصري لدورات الاحتفاظ. عند النقر على زر Memory Graph Debugger، يوقف Xcode التطبيق، ويبني رسماً بيانياً كاملاً للكائنات في الذاكرة، ويبرز دورات الاحتفاظ باللون الأحمر.
| الأداة | المنصة | الميزة |
|---|---|---|
| LeakCanary | Android | كشف تلقائي للتسربات بعد destroy |
| Memory Profiler | Android Studio | Heap dump + تخصيصات مباشرة |
| Eclipse MAT | Android | شجرة مسيطرة، Leak Suspects Report |
| Memory Graph | iOS (Xcode) | مرئي دورات الاحتفاظ |
وفقاً لـ Google I/O 2023، التطبيقات التي تستخدم LeakCanary في إصدارات التصحيح تقلل الأعطال المتعلقة بالذاكرة بنسبة 30–50% في أول شهرين بعد التبني. يُوصى بإضافة LeakCanary خلال مرحلة بدء المشروع.
الأسئلة الشائعة
التسرب — كائنات لا يمكن للكود الوصول إليها ولكن لا يتم جمعها بواسطة GC بسبب مراجع نشطة. الانتفاخ — التطبيق يحتفظ بكائنات مطلوبة منطقياً ولكن بكمية زائدة (مثلاً، ذاكرة تخزين مؤقت 50 ميغابايت في تطبيق يعمل بحجم 80 ميغابايت). الانتفاخ يُعالج معملياً، التسرب — من خلال إدارة صحيحة للمراجع.
LeakCanary يستخدم ObjectWatcher — بعد onDestroy() لـ Activity، ينشئ WeakReference إلى Activity ويشغل GC. إذا لم يتم مسح WeakReference بعد 5 ثوانٍ، يقوم LeakCanary بعمل heap dump، ويحلل أقصر سلسلة مرجعية من GC Root إلى الكائن، ويظهر مكدس التسرب الدقيق مع اسم الملف ورقم السطر.
Bitmap يشغل ذاكرة خارج heap Java في الذاكرة الأصلية (native heap). حجم Bitmap واحد = العرض × الارتفاع × 4 بايت (ARGB_8888). صورة 12 ميغابكسل (4000×3000) تشغل 48 ميغابايت. لا يستطيع Android دائماً تحرير الذاكرة الأصلية في الوقت المناسب، لذا تراكم عدة Bitmaps يؤدي إلى OOM حتى مع وجود heap Java كافٍ.
دورة الاحتفاظ — حالة في ARC حيث يحتفظ كائنان بمراجع قوية لبعضهما البعض، ولا يصل عداد المراجع أبداً إلى الصفر. مثال نموذجي: ViewController بمرجع قوي إلى closure، ويلتقط closure self بقوة. الحل: استخدم [weak self] أو [unowned self] في closures.
حجم heap يعتمد على الجهاز وإصدار Android. للأجهزة القديمة (API 15–24) — 64–128 ميغابايت. للأجهزة الحديثة (API 25+) — 256–512 ميغابايت. يمكن الحصول على القيمة الدقيقة عبر ActivityManager.getMemoryClass(). للتطبيقات الكبيرة (الألعاب، المحررات)، largeHeap=true في البيان يوفر حتى 1 غيغابايت.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.