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

نویسنده: IT Sectr منتشر شده: 2026-07-28 زمان مطالعه: 10 دقیقه

نشت حافظه — یکی از موذیانه‌ترین مشکلات در توسعه موبایل است. حافظه برنامه پیوسته افزایش می‌یابد تا به حد تعیین‌شده توسط سیستم‌عامل برسد، پس از آن OutOfMemoryError یا خاتمه اجباری رخ می‌دهد. به گفته Square Engineering، حدود 40% از برنامه‌های Android حداقل یک نشت حافظه دارند که تنها در هنگام پروفایل‌سازی قابل تشخیص است. علل و روش‌های جلوگیری از رشد حافظه را بررسی می‌کنیم.

نکات اصلی

  • GC reachability — اگر یک شیء از مجموعه ریشه ارجاع فعال داشته باشد، حذف نمی‌شود
  • ارجاعات ایستا به Activity یا Context — شایع‌ترین علت نشت در Android
  • LeakCanary — ابزار استاندارد برای تشخیص خودکار نشت در Android
  • WeakReference — راه‌حل برای ارجاعاتی که نباید مانع جمع‌آوری زباله شوند
  • کامپوننت‌های Lifecycle-aware به طور خودکار اشتراک‌ها را هنگام نابودی View لغو می‌کنند

نشت حافظه و تورم برنامه چیست؟

نشت حافظه (memory leak) — وضعیتی که یک شیء که دیگر برای برنامه مورد نیاز نیست، به دلیل وجود یک ارجاع فعال از مجموعه ریشه (GC Root) همچنان در heap باقی می‌ماند. جمع‌کننده زباله چنین شیئی را زنده در نظر گرفته و آن را حذف نمی‌کند.

تورم حافظه (memory bloat) — مشکل گسترده‌تری که در آن برنامه حافظه بیشتری نسبت به نیاز برای انجام وظایف جاری مصرف می‌کند. علل: کش‌سازی بیش از حد، تکرار اشیاء، ساختارهای داده غیربهینه و تکه‌تکه شدن heap.

در Android برای هر برنامه یک heap محدود اختصاص داده می‌شود (معمولاً 64-512 MB بسته به دستگاه و نسخه سیستم‌عامل). در iOS محدودیت کمتر سختگیرانه است، اما سیستم هنگام نزدیک شدن به حد مجاز اخطار حافظه ارسال می‌کند.

ویژگیAndroidiOS
محدودیت heap64-512 MB (بستگی به دستگاه دارد)ضمنی (سیستمی)
جمع‌آوری زبالهART (Concurrent, Compact)ARC (Automatic Reference Counting)
مکانیزم نشتارجاعات GC Rootچرخه‌های retain (چرخه‌های ارجاع قوی)
نتیجهOutOfMemoryErrorاخطار حافظه → خاتمه

به گفته Facebook Engineering Blog، نشت حافظه علت ~15% از گزارش‌های crash در برنامه‌های موبایل است. در Android به این موارد ANR ناشی از توقف‌های مکرر GC در هنگام کمبود حافظه اضافه می‌شود.

الگوهای معمول نشت حافظه در Android و iOS

ارجاع ایستا به Activity — کلاسیک نشت‌های Android. اگر یک فیلد ایستا یا singleton ارجاعی به Activity ذخیره کند، تا زمانی که singleton زنده است حتی پس از finish() توسط GC جمع‌آوری نخواهد شد. Activity یک شیء سنگین حاوی سلسله‌مراتب View، منابع و Context است.

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

  • Handler با تأخیر — اگر Activity نابود شده باشد اما Handler.postDelayed هنوز اجرا نشده باشد، Activity نشت می‌کند
  • Thread و AsyncTask — هنگام چرخش صفحه Activity دوباره ایجاد می‌شود، اما Thread قدیمی همچنان ارجاعی به Activity قدیمی نگه می‌دارد
  • Retrofit/Callback — Callback ناشناس ارجاعی به presenter یا fragment نگه می‌دارد
  • مشاهده‌گرها (Observers) — اشتراک‌های LiveData یا RxJava بدون لغو در onDestroy

در iOS مشکل اصلی retain cycles است: دو شیء ارجاعات قوی به یکدیگر نگه می‌دارند و ARC نمی‌تواند شمارنده ارجاع هیچ‌کدام را صفر کند. حالت معمول: closure که self را به طور قوی capture می‌کند و self که ارجاعی به closure نگه می‌دارد.

چگونه نشت حافظه را تشخیص دهیم؟

LeakCanary — کتابخانه‌ای از Square برای تشخیص خودکار نشت در Android. پس از نابودی Activity یا Fragment بررسی می‌کند که آیا شیء توسط GC جمع‌آوری شده است. اگر نه — heap dump گرفته و trace نشت را نشان می‌دهد.

kotlin
// 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 لغو می‌شوند که کلاس اصلی نشت‌ها را حذف می‌کند.

kotlin
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 — را حذف می‌کند.

  • استفاده نکنید از ارجاعات ایستا به Context, Activity, View یا Fragment
  • لغو کنید تمام اشتراک‌های RxJava در disposeBag / CompositeDisposable هنگام onDestroy
  • استفاده کنید از [weak self] / [unowned self] در closureهای iOS برای جلوگیری از retain cycles
  • بررسی کنید Bitmap و اشیاء بزرگ — باید بازیافت یا null شوند

ابزارهای پروفایل‌سازی حافظه

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 را با رنگ قرمز برجسته می‌کند.

ابزارپلتفرمویژگی
LeakCanaryAndroidتشخیص خودکار نشت پس از destroy
Memory ProfilerAndroid StudioHeap dump + تخصیص‌های زنده
Eclipse MATAndroidدرخت dominator، Leak Suspects Report
Memory GraphiOS (Xcode)تصویرساز retain cycles

به گفته Google I/O 2023، برنامه‌هایی که از LeakCanary در buildهای debug استفاده می‌کنند، تعداد crashهای مرتبط با حافظه را در 2 ماه اول پس از پیاده‌سازی 30-50% کاهش می‌دهند. توصیه می‌شود LeakCanary در مرحله onboarding پروژه اضافه شود.

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

تفاوت نشت حافظه و تورم چیست؟

نشت — اشیایی که برای کد قابل دسترسی نیستند اما به دلیل ارجاعات فعال توسط GC حذف نمی‌شوند. تورم — برنامه اشیایی را در حافظه نگه می‌دارد که منطقاً مورد نیاز هستند اما در مقادیر اضافی (مثلاً کش 50 MB در برنامه‌ای با وزن 80 MB). تورم به صورت معماری درمان می‌شود، نشت — از طریق مدیریت صحیح ارجاعات.

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

LeakCanary از ObjectWatcher استفاده می‌کند — پس از onDestroy() Activity یک WeakReference به Activity ایجاد کرده و GC را اجرا می‌کند. اگر بعد از 5 ثانیه WeakReference پاک نشود، LeakCanary heap dump گرفته، کوتاه‌ترین زنجیره ارجاع از GC Root تا شیء را تحلیل کرده و stack دقیق نشت با نام فایل و خط کد را نشان می‌دهد.

چرا Bitmap اغلب باعث OutOfMemoryError می‌شود؟

Bitmap حافظه را خارج از heap Java در حافظه بومی (native heap) اشغال می‌کند. اندازه یک Bitmap = عرض × ارتفاع × 4 بایت (ARGB_8888). یک عکس 12 MP (4000×3000) 48 MB اشغال می‌کند. Android همیشه نمی‌تواند به موقع حافظه بومی را آزاد کند، که با انباشته شدن چند Bitmap حتی با heap Java کافی منجر به OOM می‌شود.

retain cycle در iOS چیست؟

Retain cycle — وضعیتی در ARC که دو شیء ارجاعات قوی به یکدیگر نگه می‌دارند و شمارنده ارجاع هرگز به صفر نمی‌رسد. مثال معمول: ViewController با ارجاع قوی به closure، و closure که self را به طور قوی capture می‌کند. راه‌حل: استفاده از [weak self] یا [unowned self] در closureها.

حداکثر اندازه heap در Android چقدر است؟

اندازه heap بستگی به دستگاه و نسخه Android دارد. برای دستگاه‌های قدیمی (API 15-24) — 64-128 MB. برای دستگاه‌های مدرن (API 25+) — 256-512 MB. مقدار دقیق را می‌توان از طریق ActivityManager.getMemoryClass() به دست آورد. برای برنامه‌های بزرگ (بازی‌ها، ویرایشگرها) largeHeap=true در manifest وجود دارد که تا 1 GB می‌دهد.

خلاصه

  • نشت حافظه — شیء حذف‌نشده توسط GC به دلیل ارجاع فعال از مجموعه ریشه؛ تورم — مصرف بیش از حد حافظه بدون نشت آشکار
  • ارجاعات ایستا به Activity, Context, View — علت شماره یک نشت در Android؛ راه‌حل — WeakReference یا Application Context
  • کلاس‌های ناشناس و لامبداها به طور ضمنی ارجاع به کلاس بیرونی نگه می‌دارند؛ callbackهای لغونشده — دومین علت شایع
  • LeakCanary — استاندارد تشخیص خودکار نشت در Android؛ یکپارچه‌سازی 5 دقیقه طول می‌کشد و نرخ crash را 30-50% کاهش می‌دهد
  • lifecycleScope و viewModelScope به طور خودکار کوروتین‌ها را در destroy لغو می‌کنند و یک کلاس کامل از نشت‌ها را حذف می‌کنند
  • Retain cycles در iOS با weak/unowned self در closureها و delegateها حل می‌شوند
  • پروفایل حافظه حداقل یک بار در اسپرینت — heap dump با MAT یا Memory Graph باید بخشی از code review باشد

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

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

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

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