نشت حافظه (memory leak) — وضعیتی است که برنامه حافظه اشغال شده توسط اشیایی که دیگر مورد نیاز نیستند را آزاد نمیکند. در توسعه موبایل این به ویژه حیاتی است: heap محدود و نبود swap منجر به OutOfMemoryError و سقوط برنامه میشود. طبق دادههای Purdue University (2022)، 35٪ از برنامههای Android در Google Play حداقل دارای یک نشت حافظه هستند. سناریوهای معمول، ابزارهای تشخیص و روشهای رفع را بررسی میکنیم.
نکات اصلی
نشت حافظه — وضعیتی است که حافظه تخصیص داده شده پس از اینکه شیء دیگر برای برنامه مورد نیاز نیست به سیستم بازگردانده نمیشود. زبالهروب چنین شیئی را زنده در نظر میگیرد، زیرا یک زنجیره مرجع فعال از GC Root به آن منتهی میشود.
در Java/Kotlin زبالهروب به طور خودکار کار میکند، اما نمیتواند تعیین کند که یک شیء منطقاً مورد نیاز نیست اگر یک مرجع فنی به آن وجود داشته باشد. توسعهدهنده باید به صراحت ارتباطات غیرضروری را قطع کند. در Swift/Objective-C ARC به طور خودکار مراجع را شمارش میکند، اما retain cycles شمارشگر را صفر نمیکند.
خطر اصلی نشتها — اثر تجمعی است. هر نشت مقدار کمی حافظه مصرف میکند، اما با جابجاییهای مکرر بین صفحهها (چرخش صفحه، باز/بسته کردن Activity) نشتها انباشته میشوند تا زمانی که محدودیت heap تمام شود.
نشت — شیء برای کد قابل دسترسی نیست، اما توسط GC حذف نشده است. تورم — شیء منطقاً مورد نیاز است، اما به مقدار اضافی ذخیره شده است. مثال تورم: کش تصاویر 100 مگابایتی با مجموعه کاری 30 مگابایت. هر دو مشکل منجر به OOM میشوند، اما دلایل و روشهای درمان متفاوت است.
ART (Android Runtime) از جمعآوری زباله نسلمحور با concurrent compaction استفاده میکند. حافظه به نسل جوان (Young)، پیر (Old) و اشیای بزرگ (Large) تقسیم میشود. اشیایی که چندین چرخه GC را پشت سر گذاشتهاند به Old generation منتقل میشوند، جایی که جمعآوری کمتر انجام میشود — این کار چرخههای معمولی را سرعت میبخشد.
GC زمانی شروع میشود که heap به آستانه مشخصی از اشغال میرسد (معمولاً 75-85٪). در طول GC تمام رشتههای برنامه متوقف میشوند (STW — Stop The World). هرچه اشیای زنده بیشتر باشد، مکث طولانیتر است. نشتها تعداد اشیای زنده را افزایش میدهند و مکثهای GC را طولانیتر میکنند.
زبالهروب اشیای زنده را با پیمایش گراف از GC Roots تعیین میکند: فیلدهای ایستا، متغیرهای پشتهای رشتههای فعال، مراجع JNI. هر شیء قابل دسترسی از طریق مراجع از این ریشهها زنده در نظر گرفته میشود — حتی اگر توسعهدهنده بداند که دیگر مورد نیاز نیست.
// مثال: مجموعه ایستا به عنوان GC Root — نشت دائمی
object GlobalHolder {
val listeners = mutableListOf<WeakReference<Any>>()
}
class LeakingFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
GlobalHolder.listeners.add(WeakReference(this))
// WeakReference از GC جلوگیری نمیکند — رفتار صحیح
}
}
WeakReference مشکل را حل میکند: GC مراجع ضعیف را در تعیین اشیای زنده نادیده میگیرد. اگر فقط مراجع ضعیف به یک شیء باقی مانده باشد، در نزدیکترین چرخه GC جمعآوری میشود.
Activity Context — گستردهترین سناریوی نشت در Android. اگر singleton، فیلد ایستا یا سرویس طولانیمدت مرجعی به Activity Context ذخیره کند، کل Activity با تمام Viewها نمیتواند توسط GC جمعآوری شود. راهحل: برای اشیای طولانیمدت از Application Context استفاده کنید.
Handler و پیامهای ارسال شده — Handler.postDelayed(runnable, delay) یک پیام در صف Main Looper قرار میدهد. اگر Activity قبل از اتمام تاخیر نابود شود، پیام همچنان در صف است و مرجع را از طریق Runnable → کلاس ناشناس → کلاس خارجی (Activity) نگه میدارد.
class SafeActivity : AppCompatActivity() {
private val mainHandler = Handler(Looper.getMainLooper())
private val callback = Runnable { /* update UI */ }
override fun onResume() {
super.onResume()
mainHandler.postDelayed(callback, 5000)
}
override fun onPause() {
mainHandler.removeCallbacks(callback) // اجباری: صف را پاک کنید
super.onPause()
}
}
کلاسهای داخلی — کلاس داخلی غیرایستا یک مرجع ضمنی به نمونه کلاس خارجی دارد. اگر کلاس خارجی Activity باشد و کلاس داخلی به جایی خارج ارسال شده باشد (مثلاً به RecyclerView.Adapter)، Activity نمیتواند جمعآوری شود.
Android Studio Memory Profiler — ابزار داخلی برای نظارت بر heap در زمان واقعی. نمودار حافظه اشغال شده، تعداد تخصیصها و اشیا بر اساس نوع را نشان میدهد. امکان ضبط heap dump و خروجی به فرمت HPROF برای تحلیل در MAT را فراهم میکند.
Eclipse MAT (Memory Analyzer Tool) — تحلیلگر رومیزی heap dump. به طور خودکار Leak Suspects Report را میسازد که اشیا با بیشترین retained size را برجسته میکند و زنجیره GC root احتمالی را برای هر شیء مشکوک پیشنهاد میدهد.
Xcode Memory Graph Debugger — برای iOS. برنامه را متوقف میکند و گراف اشیا را تجسم میبخشد. Retain cycles با رنگ قرمز برجسته میشوند، میتوان روی هر شیء کلیک کرد و retain count و مراجع آن را مشاهده کرد.
| ابزار | قابلیتها | پیچیدگی |
|---|---|---|
| Memory Profiler | نمودار زمان واقعی، heap dump، ردیابی تخصیص اشیا | کم |
| Eclipse MAT | درخت غالب، مظنونان نشت، کوئریهای OQL | متوسط |
| LeakCanary | تشخیص خودکار، ردپای نشت در اعلان | حداقل |
| Xcode Memory Graph | گراف بصری retain cycles، لیست اشیای زنده | کم |
طبق Uber Engineering Blog، پیادهسازی پروفایلبندی خودکار حافظه (LeakCanary + تحلیل heap dump) در خط لوله CI/CD تعداد حوادث مرتبط با حافظه در تولید را طی 3 ماه 60٪ کاهش میدهد.
جایگزینی Context — اگر شیء بیشتر از Activity عمر میکند، از applicationContext استفاده کنید. همه اشیای طولانیمدت (singletonها، مخزنها، کمککنندههای پایگاه داده) باید Application Context دریافت کنند، نه Activity Context. استثنا: کامپوننتهای UI که به تم یا منابع خاص Activity نیاز دارند.
کامپوننتهای Lifecycle-aware — استفاده از LifecycleObserver، DefaultLifecycleObserver یا reactivex به طور خودکار اشتراکها را در onDestroy لغو میکند. Android Jetpack lifecycleScope و viewModelScope را فراهم میکند که با رویداد مربوطه پاک میشوند.
کلاس داخلی ایستا — اگر کلاس داخلی به فیلدهای کلاس خارجی نیاز ندارد، آن را static کنید. کلاس داخلی ایستا مرجع ضمنی به کلاس خارجی ندارد. اگر دسترسی لازم است، از WeakReference برای مرجع صریح استفاده کنید.
class MyActivity : AppCompatActivity() {
// ❌ کلاس داخلی غیرایستا — مرجع ضمنی به MyActivity
inner class BadListener : SomeListener {
override fun onEvent() { /*...*/ }
}
// ✅ کلاس داخلی ایستا — بدون مرجع ضمنی
class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
override fun onEvent() { /*...*/ }
}
}
در iOS از لیستهای ضبط استفاده کنید: [weak self] در بستارهایی که ممکن است بیشتر از ایجادکننده عمر کنند. برای delegateها از مراجع ضعیف استفاده کنید (weak var delegate). برای بستارهایی که تضمیناً فقط در طول عمر self فراخوانی میشوند، میتوان از [unowned self] استفاده کرد، اما با احتیاط — دسترسی به شیء آزاد شده باعث crash میشود.
سوالات متداول
در Android چند جابجایی بین صفحهها انجام دهید (Activity A → B → A → B) و adb shell dumpsys meminfo package_name را بررسی کنید. اگر Total PSS به طور پایدار افزایش یابد و به مقدار اولیه برنگردد — نشت وجود دارد. در iOS به طور مشابه: برای بررسی بصری از Debug Memory Graph در Xcode استفاده کنید.
بله، اگر CoroutineScope هنگام نابودی کامپوننت لغو نشود. کوروتین راهاندازی شده در GlobalScope حتی پس از finish() Activity به اجرا ادامه میدهد. راهحل: از viewModelScope (لغو شده در onCleared) یا lifecycleScope (لغو شده در onDestroy) استفاده کنید. برای Scopeهای سفارشی خود از طریق LifecycleOwner scopeهای lifecyle-aware ایجاد کنید.
Bitmap دادههای پیکسل را در heap بومی ذخیره میکند، نه در Java heap. این بدان معناست که Java GC اندازه واقعی Bitmap را نمیبیند. اگر recycle() روی Bitmap فراخوانی نشود یا مرجع صفر نشود، حافظه بومی آزاد نخواهد شد. برای بارگذاری نسخههای کوچکشده از BitmapFactory با inSampleSize و برای مدیریت خودکار کش از Glide/Coil استفاده کنید.
فیلد ایستا — یک GC Root است. تا زمانی که کلاس بارگذاری شده است (در Android — تا زمانی که Process زنده است) زندگی میکند. اگر فیلد ایستا به Activity، Bitmap، View یا هر شیء سنگین دیگری ارجاع دهد، این شیء هرگز توسط GC جمعآوری نخواهد شد. فیلد ایستا — یک مرجع ابدی است. راهحل: فقط WeakReference ذخیره کنید یا فیلد ایستا را در onDestroy صفر کنید.
ARC به طور خودکار اشیا را زمانی که شمارنده مراجع قوی به صفر میرسد آزاد میکند. Retain cycle — تنها راه نشت در ARC است. همیشه برای مراجع parent→child از weak استفاده کنید، جایی که child باید بیشتر از parent عمر کند (delegateها، data source). برای بستارها از لیست ضبط [weak self] استفاده کنید و self را در داخل بستار برای nil بررسی کنید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.