نشت حافظه — یکی از موذیانهترین مشکلات در توسعه موبایل است. حافظه برنامه پیوسته افزایش مییابد تا به حد تعیینشده توسط سیستمعامل برسد، پس از آن OutOfMemoryError یا خاتمه اجباری رخ میدهد. به گفته Square Engineering، حدود 40% از برنامههای Android حداقل یک نشت حافظه دارند که تنها در هنگام پروفایلسازی قابل تشخیص است. علل و روشهای جلوگیری از رشد حافظه را بررسی میکنیم.
نکات اصلی
نشت حافظه (memory leak) — وضعیتی که یک شیء که دیگر برای برنامه مورد نیاز نیست، به دلیل وجود یک ارجاع فعال از مجموعه ریشه (GC Root) همچنان در heap باقی میماند. جمعکننده زباله چنین شیئی را زنده در نظر گرفته و آن را حذف نمیکند.
تورم حافظه (memory bloat) — مشکل گستردهتری که در آن برنامه حافظه بیشتری نسبت به نیاز برای انجام وظایف جاری مصرف میکند. علل: کشسازی بیش از حد، تکرار اشیاء، ساختارهای داده غیربهینه و تکهتکه شدن heap.
در Android برای هر برنامه یک heap محدود اختصاص داده میشود (معمولاً 64-512 MB بسته به دستگاه و نسخه سیستمعامل). در iOS محدودیت کمتر سختگیرانه است، اما سیستم هنگام نزدیک شدن به حد مجاز اخطار حافظه ارسال میکند.
| ویژگی | Android | iOS |
|---|---|---|
| محدودیت heap | 64-512 MB (بستگی به دستگاه دارد) | ضمنی (سیستمی) |
| جمعآوری زباله | ART (Concurrent, Compact) | ARC (Automatic Reference Counting) |
| مکانیزم نشت | ارجاعات GC Root | چرخههای retain (چرخههای ارجاع قوی) |
| نتیجه | OutOfMemoryError | اخطار حافظه → خاتمه |
به گفته Facebook Engineering Blog، نشت حافظه علت ~15% از گزارشهای crash در برنامههای موبایل است. در Android به این موارد ANR ناشی از توقفهای مکرر GC در هنگام کمبود حافظه اضافه میشود.
ارجاع ایستا به Activity — کلاسیک نشتهای Android. اگر یک فیلد ایستا یا singleton ارجاعی به Activity ذخیره کند، تا زمانی که singleton زنده است حتی پس از finish() توسط GC جمعآوری نخواهد شد. Activity یک شیء سنگین حاوی سلسلهمراتب View، منابع و Context است.
object LeakHolder {
var activityRef: Activity ?= null // نشت: ارجاع ایستا به Activity
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
LeakHolder.activityRef = this // ❌ MainActivity هرگز توسط GC جمعآوری نخواهد شد
}
}
کلاسهای ناشناس و لامبداها — به طور ضمنی ارجاعی به کلاس بیرونی نگه میدارند. اگر Runnable یا Callback به سرویس خارجی ارسال شود و Activity نابود شود، شیء کلاس ناشناس همچنان در صف باقی میماند و اجازه جمعآوری Activity توسط GC را نمیدهد.
در iOS مشکل اصلی retain cycles است: دو شیء ارجاعات قوی به یکدیگر نگه میدارند و ARC نمیتواند شمارنده ارجاع هیچکدام را صفر کند. حالت معمول: closure که self را به طور قوی capture میکند و self که ارجاعی به closure نگه میدارد.
LeakCanary — کتابخانهای از Square برای تشخیص خودکار نشت در Android. پس از نابودی Activity یا Fragment بررسی میکند که آیا شیء توسط GC جمعآوری شده است. اگر نه — heap dump گرفته و trace نشت را نشان میدهد.
// LeakCanary 2.x — یکپارچهسازی خودکار از طریق Application
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary به طور خودکار در build debug نصب میشود
// از طریق ContentProvider — راهاندازی بدون کد
}
}
// فراخوانی بررسی اجباری
AppWatcher.objectWatcher.watch(watchedObject, "leak description")
Android Studio Profiler — ابزار داخلی برای نظارت بر حافظه در زمان واقعی. امکان ثبت heap dump، یافتن اشیاء مشکوک (Retained Size > 1 MB) و ردیابی مسیر GC root تا هر شیء را فراهم میکند.
برای iOS از Xcode Memory Graph Debugger استفاده کنید. این ابزار گراف اشیاء در حافظه را تصویرسازی میکند، retain cycles را نشان میدهد و امکان تشخیص فوری ارجاعات چرخهای را فراهم میکند. همچنین Instruments > Allocations برای نظارت طولانیمدت در دسترس است.
WeakReference — مکانیزم پایه برای ارجاعاتی که نباید مانع جمعآوری زباله شوند. اگر GC تصمیم به حذف شیء بگیرد، WeakReference مقدار null برمیگرداند. برای فراخوانیها، شنوندهها و ارجاعات به کامپوننتهای UI از رشتههای پسزمینه استفاده میشود.
کامپوننتهای Lifecycle-aware — رویکرد معماری پیادهسازی شده در 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)
// کوروتین به طور خودکار در onCleared() لغو میشود
}
}
}
viewModelScope و lifecycleScope — CoroutineScopeهای داخلی در Android که در رویداد مربوطه چرخه حیات لغو میشوند. این کار نشت از طریق کوروتینها — رایجترین سناریو در توسعه مدرن Android — را حذف میکند.
Memory Profiler in Android Studio — ابزار اصلی برای نظارت بر heap. تخصیصهای زنده، عکسهای heap، تعداد اشیاء بر اساس نوع را نشان میدهد. امکان ثبت dump و تحلیل آن در MAT (Memory Analyzer Tool) برای یافتن اشیاء مشکوک را فراهم میکند.
Eclipse MAT — تحلیلگر رومیزی heap dump. پس از بارگذاری فایل HPROF از Android Studio، MAT درخت dominator میسازد، retain size هر شیء را نشان میدهد و تحلیل خودکار نشتهای مشکوک را از طریق Leak Suspects Report ارائه میدهد.
Xcode Memory Graph — دیباگر بصری retain cycles. با کلیک دکمه Memory Graph Debugger، Xcode برنامه را متوقف میکند، گراف کامل اشیاء در حافظه را میسازد و retain cycles را با رنگ قرمز برجسته میکند.
| ابزار | پلتفرم | ویژگی |
|---|---|---|
| LeakCanary | Android | تشخیص خودکار نشت پس از destroy |
| Memory Profiler | Android Studio | Heap dump + تخصیصهای زنده |
| Eclipse MAT | Android | درخت dominator، Leak Suspects Report |
| Memory Graph | iOS (Xcode) | تصویرساز retain cycles |
به گفته Google I/O 2023، برنامههایی که از LeakCanary در buildهای debug استفاده میکنند، تعداد crashهای مرتبط با حافظه را در 2 ماه اول پس از پیادهسازی 30-50% کاهش میدهند. توصیه میشود LeakCanary در مرحله onboarding پروژه اضافه شود.
سوالات متداول
نشت — اشیایی که برای کد قابل دسترسی نیستند اما به دلیل ارجاعات فعال توسط GC حذف نمیشوند. تورم — برنامه اشیایی را در حافظه نگه میدارد که منطقاً مورد نیاز هستند اما در مقادیر اضافی (مثلاً کش 50 MB در برنامهای با وزن 80 MB). تورم به صورت معماری درمان میشود، نشت — از طریق مدیریت صحیح ارجاعات.
LeakCanary از ObjectWatcher استفاده میکند — پس از onDestroy() Activity یک WeakReference به Activity ایجاد کرده و GC را اجرا میکند. اگر بعد از 5 ثانیه WeakReference پاک نشود، LeakCanary heap dump گرفته، کوتاهترین زنجیره ارجاع از GC Root تا شیء را تحلیل کرده و stack دقیق نشت با نام فایل و خط کد را نشان میدهد.
Bitmap حافظه را خارج از heap Java در حافظه بومی (native heap) اشغال میکند. اندازه یک Bitmap = عرض × ارتفاع × 4 بایت (ARGB_8888). یک عکس 12 MP (4000×3000) 48 MB اشغال میکند. Android همیشه نمیتواند به موقع حافظه بومی را آزاد کند، که با انباشته شدن چند Bitmap حتی با heap Java کافی منجر به OOM میشود.
Retain cycle — وضعیتی در ARC که دو شیء ارجاعات قوی به یکدیگر نگه میدارند و شمارنده ارجاع هرگز به صفر نمیرسد. مثال معمول: ViewController با ارجاع قوی به closure، و closure که self را به طور قوی capture میکند. راهحل: استفاده از [weak self] یا [unowned self] در closureها.
اندازه heap بستگی به دستگاه و نسخه Android دارد. برای دستگاههای قدیمی (API 15-24) — 64-128 MB. برای دستگاههای مدرن (API 25+) — 256-512 MB. مقدار دقیق را میتوان از طریق ActivityManager.getMemoryClass() به دست آورد. برای برنامههای بزرگ (بازیها، ویرایشگرها) largeHeap=true در manifest وجود دارد که تا 1 GB میدهد.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.