نشت حافظه در برنامه‌های موبایل — چیست، علل و روش‌های تشخیص

نویسنده: IT Sectr منتشر شده: 2026-03-29 زمان مطالعه: 9 دقیقه

نشت حافظه (Memory Leak) — وضعیتی است که برنامه ارجاعاتی به اشیایی که دیگر مورد نیاز نیستند را نگه می‌دارد و اجازه نمی‌دهد زباله‌روب (Garbage Collector) occupied memory را آزاد کند. بر اساس داده‌های LeakCanary، حتی در برنامه‌های خوب نوشته شده، ۳–۵ نشت در هر ۱۰۰۰۰ خط کد یافت می‌شود. هر نشت به تدریج حافظه موجود را کاهش می‌دهد و منجر به کندی و OutOfMemoryError می‌شود.

مهم‌ترین نکات

  • Memory Leak — شیئی در حافظه باقی می‌ماند، اگرچه هیچ ارجاع فعالی از منطق برنامه به آن وجود ندارد
  • ارجاعات ایستا به Activity یا Context — شایع‌ترین علت نشت در اندروید
  • LeakCanary — ابزار استاندارد برای تشخیص خودکار نشت در اندروید
  • WeakReference و Application Context — تکنیک‌های پایه جلوگیری از نشت
  • کامپوننت‌های Lifecycle-aware یک کلاس کامل از نشت‌های مرتبط با اشتراک‌ها را از بین می‌برند

نشت حافظه چیست

نشت حافظه (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 یا Context. کد معمول: یک فیلد ایستا Activity که در onDestroy() صفر نمی‌شود. تا زمانی که فیلد ایستا زنده است، کل Activity با درخت View خود که می‌تواند ۱–۱۰ مگابایت اشغال کند، زنده می‌ماند. این یک نشت کلاسیک است که LeakCanary در وهله اول پیدا می‌کند.

راه حل: هرگز Activity یا Context را در فیلدهای ایستا ذخیره نکنید. برای singletonهایی که بیشتر از Activity عمر می‌کنند از Application Context استفاده کنید. اگر به Activity نیاز دارید — از WeakReference<Activity> استفاده کنید.

kotlin
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() به چرخه حیات متصل است.

kotlin
// لغو اشتراک خودکار از طریق Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// کوروتین‌ها با lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap بدون recycle

Bitmap مقدار قابل توجهی از حافظه Heap را اشغال می‌کند: یک FullHD-bitmap — ۱۹۲۰ × ۱۰۸۰ × ۴ بایت = ۸.۳ مگابایت. اگر برای هر عنصر لیست Bitmap ایجاد شود و هنگام پنهان شدن recycle() فراخوانی نشود، حافظه به سرعت تمام می‌شود. در نسخه‌های قدیمی اندروید (قبل از ۳.۰)، Bitmap در حافظه native ذخیره می‌شد، اما در نسخه‌های مدرن — در Heap Dalvik/ART، و GC فقط در صورت نبود strong reference می‌تواند آن را آزاد کند.

برای بارگذاری تصاویر از Glide یا Coil استفاده کنید — این کتابخانه‌ها کش کردن و recycle را خودکار مدیریت می‌کنند. اگر مستقیماً با Bitmap کار می‌کنید، برای تصاویر بزرگی که دیگر نمایش داده نمی‌شوند bitmap.recycle() را فراخوانی کنید و برای بارگذاری نسخه‌های کوچک‌شده از inSampleSize استفاده کنید.

ارجاع به Fragment بعد از onDestroyView

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 را نگه نمی‌دارد.

معماری Lifecycle-aware

ViewModel و LiveData از Android Architecture Components مشکل چرخه حیات را در سطح معماری حل می‌کنند. ViewModel از چرخش صفحه جان سالم به در می‌برد و شامل ارجاعات به View نیست. LiveData به طور خودکار observer را در onDestroy() لغو اشتراک می‌کند. به جای اشتراک دستی در سرویس‌های سیستمی از آنها استفاده کنید.

Code Review با تمرکز بر GC Root

در code review به موارد زیر توجه کنید: فیلدهای ایستا با انواع Context/View، کلاس‌های ناشناس، لامبداهایی که Activity را می‌بندند، اشتراک‌های دستی، RxJava disposable بدون composite، ذخیره Fragment از طریق Bundle. در Kotlin علاوه بر این، coroutineها را از نظر launch بدون اتصال به چرخه حیات بررسی کنید.

بررسی خودکار در CI

LeakCanary می‌تواند به عنوان بخشی از خط لوله تست کار کند: تست‌های پذیرش را با LeakCanary اجرا کنید و در صورت یافتن نشت، build را ناموفق اعلام کنید. این از ورود نشت‌ها به تولید جلوگیری می‌کند. بررسی را با Android Lint با قاعده StaticFieldLeak تکمیل کنید — این قاعده نشت‌های بالقوه را در سطح تحلیل ایستا پیدا می‌کند.

kotlin
// LeakCanary در تست‌ها
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // اگر نشت وجود دارد fail
    }
}

سوالات متداول

نشت حافظه چه تفاوتی با OutOfMemoryError دارد؟

نشت علت است و OutOfMemoryError نتیجه. یک نشت منجر به OOM نمی‌شود، اما انباشت ده‌ها نشت Heap را تمام می‌کند. OOM یک استثنای مرگبار است و نشت الگویی است که به مرور زمان به آن منجر می‌شود.

چگونه بدون LeakCanary نشت را پیدا کنیم؟

از طریق Android Memory Profiler: صفحه را ۵ بار باز و بسته کنید، بعد از هر بار بسته شدن GC را فراخوانی کنید. اگر حافظه به سطح پایه بازنگردد — نشت وجود دارد. Heap Dump بگیرید و در لیست کلاس Activity را پیدا کنید که تعداد آن بعد از بسته شدن بیشتر از ۰ است.

آیا Kotlin می‌تواند در سطح زبان از نشت جلوگیری کند؟

تا حدی. Kotlin مشکل null-safety را حل می‌کند، اما strong references را مدیریت نمی‌کند. Coroutines با lifecycleScope و viewModelScope از نشت‌های ناشی از وظایف پس‌زمینه جلوگیری می‌کنند و sealed class و data class تعداد حالت‌های منجر به نشت را کاهش می‌دهند. محافظت اصلی — الگوهای معماری است، نه ویژگی‌های زبان.

چرا LeakCanary نشتی را پیدا می‌کند که وجود ندارد؟

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 بین اشیاء همان نشت را ایجاد می‌کند.

خلاصه

  • Memory Leak — شیئی که GC به دلیل strong reference فراموش شده نمی‌تواند آزاد کند
  • ارجاعات ایستا به Activity و Context — شایع‌ترین علت نشت
  • ارجاعات ضمنی از طریق کلاس‌های ناشناس، لامبداها و اشتراک‌های RxJava موذی‌تر از ارجاعات آشکار هستند
  • LeakCanary به طور خودکار نشت‌ها را پیدا می‌کند و stack trace دقیق نشان می‌دهد
  • کامپوننت‌های Lifecycle-aware (ViewModel، LiveData، lifecycleScope) یک کلاس نشت را از بین می‌برند
  • Heap Dump و تحلیل Retained Size — روش اصلی تشخیص دستی
  • پیشگیری شامل code review بر strong reference و بررسی CI با LeakCanary است

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید