LeakCanary هي مكتبة مفتوحة المصدر من Square للكشف التلقائي عن تسربات الذاكرة في تطبيقات Android. تدمج في عملية التطوير وت追踪 دورة حياة Activity وFragment وViewModel والمكونات الأخرى في الوقت الفعلي، مشيرة إلى التسربات فور حدوثها. وفقاً لـ Square Open Source، تُستخدم المكتبة في آلاف المشاريع وتعتبر المعيار الفعلي لتشخيص الذاكرة على Android.
الخلاصة
LeakCanary هي مكتبة للكشف التلقائي عن تسربات الذاكرة في تطبيقات Android، طورتها Square. تدمج في عملية بناء التطبيق وت追踪 تلقائياً ما إذا كانت الكائنات التي يجب تدميرها (Activity وFragment وView) تبقى في الذاكرة. عند اكتشاف تسرب، ينشئ LeakCanary heap dump ويحلل سلسلة المراجع التي تمسك بالكائن.
أصبحت المكتبة معياراً في مجتمع Android: وفقاً لـ GitHub، حصل المشروع على أكثر من 28 ألف نجمة ويُستخدم في تطبيقات Google وUber وAirbnb وFacebook. يتوفر LeakCanary بنسختين رئيسيتين: الكلاسيكية 1.x (مع إعداد يدوي) والحديثة 2.x (دمج تلقائي عبر ContentProvider). الإصدار 2.x لا يتطلب تعديل كلاس Application — التبعية كافية للتشغيل الكامل.
المهمة الرئيسية لـ LeakCanary هي اكتشاف متى يستمر كائن في الوجود في الذاكرة بعد انتهاء دورة حياته. هذا نموذجي للتسربات عبر الحقول الثابتة وsingletons والاستدعاءات غير المسجلة والكلاسات المجهولة والclosures التي تلتقط كائنات خارجية.
تسربات الذاكرة على Android أكثر خطورة من سطح المكتب بسبب محدودية RAM على الأجهزة المحمولة. حتى تسرب بحجم 5–10 ميجابايت في كل انتقال بين الشاشات يمكن أن يؤدي إلى OutOfMemoryError بعد 30–40 دقيقة من استخدام التطبيق. يكتشف LeakCanary هذه المشكلات في مرحلة التطوير، دون انتظار تعطل في الإنتاج.
LeakCanary يستخدم المراجع الضعيفة (WeakReference) مع جمع القمامة القسري. عندما تستدعي Activity أو Fragment onDestroy، ينشئ LeakCanary WeakReference لذلك الكائن ويشغل GC بعد تأخير قصير (5 ثوانٍ افتراضياً). إذا كان الكائن لا يزال قابلاً للوصول عبر WeakReference بعد GC، فهذا يعني أنه محتفظ به بواسطة مرجع قوي — يتم تسجيل تسرب.
بعد اكتشاف تسرب، يقوم LeakCanary بعمل heap dump (تفريغ الذاكرة) — لقطة كاملة لذاكرة التطبيق بتنسيق HPROF. بعد ذلك، يبني المحلل المدمج (Shark للإصدار 2.x) رسم بياني للوصول من GC Roots إلى الكائن المسرب ويجد أقصر مسار — سلسلة المراجع التي تمسك بالكائن في الذاكرة.
// منطق كشف LeakCanary المبسط
class ObjectWatcher {
private val watchedReferences = CopyOnWriteArrayList<KeyedWeakReference>()
fun watch(watchedObject: Any, description: String) {
val reference = KeyedWeakReference(watchedObject, description)
watchedReferences.add(reference)
BackgroundHandler.postDelayed({
checkForLeaks()
}, 5000)
}
private fun checkForLeaks() {
GcTrigger.runGc() // GC قسري
for (ref in watchedReferences) {
if (ref.get() != null) {
onLeakFound(ref) // الكائن نجا من GC — هذا تسرب
}
}
}
}
النقطة الرئيسية هي الاستدعاء القسري لـ GcTrigger.runGc(). بدونه، من المستحيل التمييز بين كائن تسرب فعلياً وكائن لم يجمعه GC بعد. يقوم LeakCanary بذلك حتى ثلاث مرات: إذا بقي الكائن في الذاكرة بعد ثلاث دورات GC، يتم تأكيد التسرب.
Shark هو محلل heap dump المدمج في LeakCanary 2.x، مكتوب بلغة Kotlin. على عكس المحلل السابق HAHA، لا يقوم Shark بتحميل ملف HPROF بأكمله في الذاكرة، بل يجتاز رسمه البياني للكائنات بأقل تخصيصات. هذا يقلل استهلاك RAM أثناء التحليل من 50 ميجابايت إلى 2–5 ميجابايت ويختصر وقت التحليل من 30 ثانية إلى 1–3 ثوانٍ.
تثبيت LeakCanary 2.x في مشروع Android حديث يتطلب سطراً واحداً في build.gradle. تستخدم المكتبة ContentProvider للتهيئة التلقائية — لا حاجة لتعديل كلاس Application أو إضافة أي كود إلى MainActivity. تضاف التبعية فقط لبناءات debug بحيث لا تحتوي APK الإصدار على كود إضافي.
// build.gradle (app/module)
dependencies {
// debugImplementation — المكتبة فقط لبناءات debug
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.14"
}
بعد إضافة التبعية وإعادة بناء المشروع، يظهر LeakCanary تلقائياً في التطبيق. عند التشغيل الأول، تظهر المكتبة إشعار نظام يؤكد التفعيل. جميع التسربات المكتشفة تظهر كإشعارات — النقر على الإشعار يفتح شاشة مع تقرير مفصل (LeakTrace).
للتخصيص، يمكنك إنشاء AppWatcherInstaller خاص بك وتجاوز المعاملات: مهلة GC، قائمة أنواع الكائنات المت追踪ة، تفعيل حفظ heap dump على القرص. ومع ذلك، بالنسبة لـ 90% من المشاريع، التكوين الافتراضي هو الأمثل.
ابتداءً من الإصدار 2.12، يدعم LeakCanary الت追踪 التلقائي لـ ViewModel ونطاقات coroutines وكائنات State في Compose. لا حاجة لتبعيات إضافية — تكتشف المكتبة تلقائياً مكونات Jetpack المستخدمة في المشروع وتفعل الكاشفات المقابلة.
تقرير LeakCanary (LeakTrace) هو سلسلة مراجع متعددة الأسطر من GC Root إلى الكائن المسرب. كل سطر يظهر الكلاس والحقل الذي يمر عبره المرجع القوي. يجب على المطور قراءة السلسلة من الأسفل إلى الأعلى: السطر السفلي هو الكائن المسرب، السطر العلوي هو نقطة الدخول (GC Root).
يبدو LeakTrace النموذجي هكذا: GC Root → حقل ثابت لـ Application → singleton → استدعاء → Activity. إذا رأى المطور هذه السلسلة، المشكلة واضحة: singleton يحمل استدعاءً التقط مرجعاً إلى Activity. الحل هو استبدال المرجع القوي بمرجع ضعيف في singleton.
┬
├─ android.app.Application
│ Leaking: NO (Application — singleton)
│ ↓ Application.leakedActivities
├─ java.util.ArrayList
│ Leaking: NO (ArrayList — normal)
│ ↓ ArrayList[0]
├─ com.example.MainActivity
│ Leaking: YES (Activity destroyed but still in memory)
│ ↓ MainActivity.mCallback
├─ com.example.CallbackWrapper
│ Leaking: UNKNOWN
│ ↓ CallbackWrapper.mListener
│ ~~~~~~~~~~
├─ com.example.MyCallback (anonymous)
│ Leaking: UNKNOWN
│ ↓ MyCallback.this$0
├─ com.example.MainActivity
│ Leaking: YES (MainActivity is the leak)
╰
في هذا المثال، يظهر LeakCanary أن MainActivity محتفظ بها عبر السلسلة: Application → ArrayList → MainActivity → CallbackWrapper → MyCallback → MainActivity مرة أخرى. السهم this$0 يشير إلى أن الكلاس المجهول MyCallback التقط مرجعاً خارجياً إلى Activity. الحل هو جعل الاستدعاء مرجعاً ضعيفاً أو إلغاءه في onDestroy.
يظهر LeakCanary أيضاً حالة التسرب لكل عنصر في السلسلة: NO (لا يوجد تسرب — عنصر جذر)، YES (يجب تدمير الكائن)، UNKNOWN (لم يتم تحديد الحالة). حالة UNKNOWN لا تعني مشكلة — إنه كائن وسيط لا يستطيع LeakCanary تصنيفه بشكل قاطع.
كان الانتقال من الإصدار 1.x إلى 2.x جذرياً: أعاد المطورون كتابة المكتبة من الصفر، مستبدلين المحلل القديم HAHA بمحركهم الخاص Shark، المكتوب بلغة Kotlin. Shark أسرع بمرتبة كاملة، ويتطلب ذاكرة أقل للتحليل، ويحدد بدقة أكبر الأسباب الجذرية للتسربات.
| المعامل | LeakCanary 1.x | LeakCanary 2.x |
|---|---|---|
| لغة المحلل | Java (HAHA — fork من Android SDK) | Kotlin (Shark — محرك خاص) |
| التثبيت | إعداد يدوي لـ AppWatcher في Application | تلقائي عبر ContentProvider |
| السرعة | 10–30 ثانية لتحليل heap dump | 1–5 ثوانٍ لتحليل heap dump |
| الأداء | يستهلك 10–50 ميجابايت RAM أثناء التحليل | يستهلك 2–10 ميجابايت RAM أثناء التحليل |
الميزة الرئيسية لـ Shark هي أنه لا يحمل heap dump بأكمله في الذاكرة، بل يجتاز رسمه البياني للمراجع بأقل تخصيصات. هذا يجعل LeakCanary 2.x مناسباً للاستخدام على الأجهزة ذات RAM المنخفض دون خطر OutOfMemoryError أثناء التحليل.
أضاف الإصدار 2.x أيضاً القدرة على تصدير heap dumps إلى ملف للتحليل لاحقاً في Android Studio Memory Profiler. للقيام بذلك، فعّل إعداد dumpHeapWhenLeakFound في تكوين AppWatcher.
LeakCanary يكتشف بفعالية عدة فئات من التسربات الشائعة في Android. الأكثر شيوعاً هو التسرب عبر المراجع الثابتة إلى Activity — يحتفظ المطورون بمرجع لسياق Activity في singleton، ولا يمكن لـ Activity جمعها بواسطة GC بعد انتهاء دورة حياتها.
الفئة الثانية الأكثر شيوعاً هي التسربات عبر المستمعين غير المسجلين. إذا تم استدعاء registerListener في onStart ولكن لم يتم استدعاء unregisterListener في onStop/onDestroy، يبقى كائن المستمع محتفظاً به بواسطة النظام حتى بعد تدمير النشاط. يظهر LeakCanary بوضوح أي مستمع وفي أي خدمة نظام بقي حياً.
// تسرب نموذجي: Activity ملتقطة في استدعاء singleton
object AnalyticsManager {
private var callback: ((String) -> Unit)? = null
fun register(callback: (String) -> Unit) {
this.callback = callback // مرجع قوي للاستدعاء
}
fun unregister() {
callback = null // لا تنسَ الاستدعاء في onDestroy!
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
AnalyticsManager.register { event ->
logEvent(event) // lambda تلتقط this
}
// إذا لم يُستدع unregister في onDestroy → تسرب Activity
}
}
الفئة الثالثة هي التسربات عبر Fragment في BackStack. إذا تم استدعاء FragmentTransaction.addToBackStack() دون إزالة Fragment عند العودة، تبقى نسخ Fragment القديمة في الذاكرة. يساعد LeakCanary في اكتشاف هذه التسربات المخفية في المراحل المبكرة من التطوير.
لكل تسرب مكتشف، يقدم LeakCanary وصفاً وتوصيات للإصلاح. أضاف الإصدار 2.14 تكاملاً مع Android Lint — يمكن للمكتبة إنشاء مهام تلقائياً في نظام ت追踪 المشكلات عند اكتشاف تسرب في CI.
الأسئلة الشائعة
نعم، بالتأكيد. يُضاف LeakCanary عبر debugImplementation في build.gradle، مما يستبعده تلقائياً من بناءات الإصدار. إذا تم استخدام implementation، ستضم المكتبة في APK الإصدار وستظهر التسربات للمستخدمين النهائيين — هذا غير مقبول.
التأثير على الأداء ضئيل. LeakCanary ينشط فقط بعد onDestroy للمكون ولا يتدخل في rendering الواجهة أو معالجة اللمسات. التكلفة الوحيدة هي وقفة قصيرة لـ GC القسري (حوالي 100 مللي ثانية) وكتابة heap dump عند حدوث تسرب (أجزاء من الثانية).
يحفظ LeakCanary تلقائياً heap dumps بتنسيق HPROF في مجلد التطبيق. يمكن تصدير الملف عبر Android Studio: Device File Explorer → data/data/com.example/files/leakcanary/. لعرضه، افتح الملف في Memory Profiler عبر Capture → Open Heap Dump.
نعم، ابتداءً من الإصدار 2.12 يدعم LeakCanary بالكامل Jetpack Compose. تتتبع المكتبة سياقات Composition وكائنات State، وتكتشف تلقائياً التسربات في دوال Composable. لا حاجة لإعداد منفصل — يعمل مباشرة.
النتائج الإيجابية الخاطئة ممكنة ولكنها نادرة. يستخدم LeakCanary استدعاء GC ثلاثياً قبل الإعلان عن تسرب، مما يزيل معظم النتائج الإيجابية الخاطئة. إذا كنت تعتقد أن الكشف خاطئ، أنشئ IgnoredReference للكلاس المحدد في التكوين.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا