نشت حافظه (Memory Leak) — وضعیتی است که برنامه ارجاعاتی به اشیایی که دیگر مورد نیاز نیستند را نگه میدارد و اجازه نمیدهد زبالهروب (Garbage Collector) occupied memory را آزاد کند. بر اساس دادههای LeakCanary، حتی در برنامههای خوب نوشته شده، ۳–۵ نشت در هر ۱۰۰۰۰ خط کد یافت میشود. هر نشت به تدریج حافظه موجود را کاهش میدهد و منجر به کندی و OutOfMemoryError میشود.
مهمترین نکات
نشت حافظه (Memory Leak) — وضعیتی است که یک شی از طریق زنجیرهای از ارجاعات قوی (Strong Reference) قابل دسترسی باقی میماند، در حالی که منطقاً دیگر برای برنامه مورد نیاز نیست. زبالهروب (GC) چنین شیئی را زنده در نظر میگیرد و حافظه اشغال شده توسط آن را آزاد نمیکند. در نتیجه، حافظه موجود Heap به طور مداوم کاهش مییابد و تعداد مکثهای GC افزایش مییابد.
بر خلاف زبانهایی با مدیریت دستی حافظه (C, C++)، در Java/Kotlin نشت یک free() فراموش شده نیست، بلکه یک ارجاع فراموش شده است. تا زمانی که یک strong reference از شی ریشه (GC Root) تا شی نشتکننده وجود دارد، GC آن را مورد نیاز میداند. GC Rootهای معمول: فیلدهای ایستا، رشتههای فعال، پشته فراخوانی، ارجاعات سراسری JNI.
خطر نشتها — اثر تجمعی آنهاست. یک نشت ۱۰۰ کیلوبایتی قابل توجه نیست، اما ۱۰۰ نشت از این دست ۱۰ مگابایت اشغال میکنند و برنامه به دلیل GCهای مکرر شروع به کند شدن میکند. انباشت بحرانی نشتها منجر به OutOfMemoryError و crash برنامه میشود. علائم نشت: افزایش مداوم مصرف حافظه در نمودار Profiler، مکثهای مکرر GC با STW (Stop The World) و کاهش عملکرد UI.
پنج نوع نشت ۹۵٪ موارد را در توسعه موبایل پوشش میدهد. هر کدام علت و الگوی مشخص خود را در کد دارند.
معروفترین نشت در اندروید — نگهداری یک ارجاع ایستا به Activity یا Context. کد معمول: یک فیلد ایستا Activity که در onDestroy() صفر نمیشود. تا زمانی که فیلد ایستا زنده است، کل Activity با درخت View خود که میتواند ۱–۱۰ مگابایت اشغال کند، زنده میماند. این یک نشت کلاسیک است که LeakCanary در وهله اول پیدا میکند.
راه حل: هرگز Activity یا Context را در فیلدهای ایستا ذخیره نکنید. برای singletonهایی که بیشتر از Activity عمر میکنند از Application Context استفاده کنید. اگر به Activity نیاز دارید — از WeakReference<Activity> استفاده کنید.
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
کلاسهای ناشناس و کلاسهای تو در توی غیرایستا به طور ضمنی یک ارجاع به کلاس شامل نگه میدارند. Runnableای که به Handler ارسال میشود و بعد از onDestroy() اجرا میشود، کل Activity را نگه میدارد. Callback Retrofit که Activity را میبندد نیز همین کار را میکند. این موذیترین نوع نشت است — ارجاع ضمنی در کد قابل مشاهده نیست.
عبارتهای object و لامبداهای Kotlin نیز ارجاعات به کلاس خارجی را ضبط میکنند. کلاسهای تو در تو را ایستا کنید (یا در Kotlin top-level) و ارجاعات خارجی را از طریق WeakReference منتقل کنید. برای لامبداها از رویکرد Lifecycle-aware با viewLifecycleOwner استفاده کنید.
اشتراک در سرویسهای سیستمی بدون لغو اشتراک — نشت مستقیم. SensorManager، LocationManager، NotificationListener که در onResume() ثبت شدهاند بدون فراخوانی unregister در onPause()، Activity را نگه میدارند. به طور مشابه: RxJava Disposable اضافه نشده به CompositeDisposable و coroutine راهاندازی شده از طریق GlobalScope.
از کامپوننتهای Lifecycle-aware استفاده کنید: observe() با LifecycleOwner به طور خودکار در onDestroy() لغو اشتراک میکند. برای RxJava — viewLifecycleOwner.lifecycle.addObserver با DisposableObserver. برای coroutines — lifecycleScope.launch() به چرخه حیات متصل است.
// لغو اشتراک خودکار از طریق Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// کوروتینها با lifecycleScope
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
Bitmap مقدار قابل توجهی از حافظه Heap را اشغال میکند: یک FullHD-bitmap — ۱۹۲۰ × ۱۰۸۰ × ۴ بایت = ۸.۳ مگابایت. اگر برای هر عنصر لیست Bitmap ایجاد شود و هنگام پنهان شدن recycle() فراخوانی نشود، حافظه به سرعت تمام میشود. در نسخههای قدیمی اندروید (قبل از ۳.۰)، Bitmap در حافظه native ذخیره میشد، اما در نسخههای مدرن — در Heap Dalvik/ART، و GC فقط در صورت نبود strong reference میتواند آن را آزاد کند.
برای بارگذاری تصاویر از Glide یا Coil استفاده کنید — این کتابخانهها کش کردن و recycle را خودکار مدیریت میکنند. اگر مستقیماً با Bitmap کار میکنید، برای تصاویر بزرگی که دیگر نمایش داده نمیشوند bitmap.recycle() را فراخوانی کنید و برای بارگذاری نسخههای کوچکشده از inSampleSize استفاده کنید.
Fragment دو چرخه حیات دارد: خود Fragment و View آن. بعد از onDestroyView() درخت View از بین میرود، اما خود Fragment ممکن است در حافظه باقی بماند اگر یک ارجاع خارجی وجود داشته باشد. خطای معمول — نگهداری ارجاع به Fragment در آداپتور ViewPager یا در گراف ناوبری که هنگام نابودی پاک نمیشود.
هرگز ارجاع به Fragment را در فیلدهای اشیاء طولانیعمر ذخیره نکنید. برای fragmentهای تو در تو از childFragmentManager و برای انتقال داده بین آنها از observe() با LifecycleOwner استفاده کنید. ViewPager2 این مشکل را در سطح API حل کرده است: FragmentTransactionAdapter به درستی چرخه حیات را مدیریت میکند.
تشخیص نشت مستلزم بررسی دو واقعیت است: حافظه بعد از expected lifetime بازنمیگردد و تعداد اشیاء از یک نوع خاص بدون کاهش افزایش مییابد. فرآیند تشخیص شامل سه مرحله است.
مرحله اول — بررسی بصری از طریق Memory Profiler در Android Studio. برگه Memory را باز کنید، اقدام مورد نظر را انجام دهید (صفحه را باز و بسته کنید)، GC (Garbage Collection) را فشار دهید و ببینید آیا حافظه به سطح اولیه بازگشته است. اگر بعد از ۳–۴ چرخه باز و بسته کردن حافظه به طور مداوم افزایش یابد — نشت وجود دارد.
مرحله دوم — گرفتن Heap Dump. در Memory Profiler دکمه Dump Java Heap را بزنید. فایل .hprof به دست آمده را در Android Studio باز کنید: همه اشیاء موجود در Heap را با اندازهها و ارجاعات خواهید دید. به دنبال کلاسهایی بگردید که تعداد آنها بعد از بسته شدن صفحه باید صفر باشد. مثلاً MainActivity با تعداد ۲ بعد از بسته شدن — نشت آشکار.
مرحله سوم — تحلیل Retained Size و GC Root. در Android Studio Retained Size را تحلیل کنید: چه مقدار حافظه با حذف این شی آزاد میشود. مسیر از GC Root تا شی نشان میدهد چه چیزی آن را نگه داشته: Static field → HashMap → Activity — و شما نقطه نشت را میبینید. پنل Reference widget تمام نگهدارندههای شی را نشان میدهد.
چهار ابزار تشخیص نشت را از تشخیص خودکار تا تحلیل عمیق Heap Dump پوشش میدهند.
| ابزار | روش | فرمت نتایج |
|---|---|---|
| LeakCanary | پایش خودکار | Heap Dump + stack trace نشت |
| Android Memory Profiler | پایش دستی | نمودار حافظه + Heap Dump |
| MAT (Eclipse) | تحلیل عمیق | گزارش Dominator Tree + مسیر GC Root |
| Perfetto | ردیابی سیستم | جدول زمانی + حافظه native |
LeakCanary — must-have برای هر پروژه اندروید. به طور خودکار پس از پایان چرخه حیات Activity/Fragment نشتها را تشخیص میدهد و مکان دقیق نشت را با stack trace نشان میدهد. ادغام: یک خط در build.gradle. LeakCanary 2.x نیاز به مقداردهی دستی ندارد — به طور خودکار Application Watcher را ثبت میکند.
پیشگیری از نشتها از طریق مجموعهای از قوانین و ابزارهایی که کد را در هر مرحله بررسی میکنند، در فرآیند توسعه گنجانده میشود.
هرگز ارجاع به Activity، Fragment یا View را در فیلد ایستا، singleton یا شی طولانیعمر ذخیره نکنید. اگر بدون ارجاع نمیتوان کار کرد — از WeakReference استفاده کنید یا دادهها را از طریق ViewModel ذخیره کنید که دقیقاً به اندازه نیاز عمر میکند و مستقیماً View را نگه نمیدارد.
ViewModel و LiveData از Android Architecture Components مشکل چرخه حیات را در سطح معماری حل میکنند. ViewModel از چرخش صفحه جان سالم به در میبرد و شامل ارجاعات به View نیست. LiveData به طور خودکار observer را در onDestroy() لغو اشتراک میکند. به جای اشتراک دستی در سرویسهای سیستمی از آنها استفاده کنید.
در code review به موارد زیر توجه کنید: فیلدهای ایستا با انواع Context/View، کلاسهای ناشناس، لامبداهایی که Activity را میبندند، اشتراکهای دستی، RxJava disposable بدون composite، ذخیره Fragment از طریق Bundle. در Kotlin علاوه بر این، coroutineها را از نظر launch بدون اتصال به چرخه حیات بررسی کنید.
LeakCanary میتواند به عنوان بخشی از خط لوله تست کار کند: تستهای پذیرش را با LeakCanary اجرا کنید و در صورت یافتن نشت، build را ناموفق اعلام کنید. این از ورود نشتها به تولید جلوگیری میکند. بررسی را با Android Lint با قاعده StaticFieldLeak تکمیل کنید — این قاعده نشتهای بالقوه را در سطح تحلیل ایستا پیدا میکند.
// LeakCanary در تستها
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // اگر نشت وجود دارد fail
}
}
سوالات متداول
نشت علت است و OutOfMemoryError نتیجه. یک نشت منجر به OOM نمیشود، اما انباشت دهها نشت Heap را تمام میکند. OOM یک استثنای مرگبار است و نشت الگویی است که به مرور زمان به آن منجر میشود.
از طریق Android Memory Profiler: صفحه را ۵ بار باز و بسته کنید، بعد از هر بار بسته شدن GC را فراخوانی کنید. اگر حافظه به سطح پایه بازنگردد — نشت وجود دارد. Heap Dump بگیرید و در لیست کلاس Activity را پیدا کنید که تعداد آن بعد از بسته شدن بیشتر از ۰ است.
تا حدی. Kotlin مشکل null-safety را حل میکند، اما strong references را مدیریت نمیکند. Coroutines با lifecycleScope و viewModelScope از نشتهای ناشی از وظایف پسزمینه جلوگیری میکنند و sealed class و data class تعداد حالتهای منجر به نشت را کاهش میدهند. محافظت اصلی — الگوهای معماری است، نه ویژگیهای زبان.
LeakCanary گاهی false positive میدهد: یک شی ممکن است به طور موقت توسط سیستم نگه داشته شود (مثلاً InputMethodManager آخرین View را نگه میدارد). دستی بررسی کنید: اگر Retained Size < ۱ KB و GC Root یک سرویس سیستمی است، احتمالاً این یک هشدار نادرست است.
خیر. نشت در هر پلتفرمی با GC ممکن است: iOS (Swift/Objective-C)، Flutter (Dart)، مرورگرهای وب (JavaScript). مکانیسمها یکسان هستند — strong reference از GC Root. در iOS ARC به طور خودکار حافظه را مدیریت میکند، but retain cycle بین اشیاء همان نشت را ایجاد میکند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید