Retain Cycle — ماهیت، علل ایجاد و رفع آن در توسعه برنامه‌ها

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

Retain Cycle (ارجاع چرخه‌ای) — وضعیتی در ARC که دو یا چند شیء از طریق strong referenceها به یکدیگر ارجاع می‌دهند و یک چرخه بسته تشکیل می‌دهند. بر اساس Apple Memory Management Guide, 2026، retain cycle آزادسازی همه اشیاء در چرخه را مسدود می‌کند، زیرا هر یک retain count ≥ 1 دارند. بر خلاف نشت حافظه در GC، retain cycle تضمین می‌کند اشیاء تا زمانی که حداقل یک شرکت‌کننده خارجی چرخه زنده است، زنده بمانند — و حتی پس از از دست دادن همه ارجاعات خارجی، اگر چرخه ایزوله شده باشد.

نکات اصلی

  • Retain Cycle — زنجیره بسته strong referenceها که در آن اشیاء نمی‌توانند توسط ARC آزاد شوند
  • علت — دو (یا بیشتر) شیء strong reference به یکدیگر دارند، صفر شدن retain count غیرممکن است
  • نتایج — نشت حافظه: اشیاء برای همیشه در حافظه باقی می‌مانند، مصرف RAM افزایش می‌یابد
  • راه‌حل — جایگزینی یکی از strong referenceها در چرخه با weak یا unowned
  • تشخیص — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

Retain Cycle چیست؟

Retain Cycle — وضعیتی است که در آن دو یا چند شیء از طریق strong referenceها به یکدیگر وابسته می‌شوند و یک گراف وابستگی بسته ایجاد می‌کنند. ARC نمی‌تواند هیچ‌یک از این اشیاء را آزاد کند، زیرا retain count هر یک همیشه ≥ 1 است: شیء A، B را نگه می‌دارد، B، A را نگه می‌دارد و شمارنده‌های آنها هرگز صفر نمی‌شوند.

این مشکل منحصراً در سیستم‌های شمارش مراجع (ARC, MRR) رخ می‌دهد. در Garbage Collection، جمع‌آورنده بر اساس گراف ارجاعات از ریشه (root set) عدم دسترسی را تعیین می‌کند — چرخه‌ها مانعی نیستند. در ARC اما چرخه معادل نشت است، زیرا آزادسازی قطعی مبتنی بر شمارنده نمی‌تواند وابستگی چرخه‌ای را حل کند.

بر اساس WWDC 2012 Session 406، retain cycle شایع‌ترین علت نشت حافظه در برنامه‌های Objective-C و Swift است. سناریوهای معمول: روابط parent-child با delegateها، بسته‌هایی که self را می‌گیرند و معماری‌های لایه‌ای با ارتباطات دوطرفه.

نمونه‌های retain cycle در توسعه iOS

سناریوهای کلاسیک retain cycle را که هر توسعه‌دهنده iOS با آن مواجه می‌شود بررسی می‌کنیم. درک این الگوها اساس نوشتن کد امن با ARC است.

Parent-Child با delegate

سناریوی کلاسیک: شیء والد (مثلاً UIViewController) یک شیء فرزند ایجاد می‌کند و delegate آن می‌شود. اگر هر دو از strong reference استفاده کنند، retain cycle ایجاد می‌شود. راه‌حل — delegate باید weak باشد.

swift
// خطا: retain cycle از طریق strong delegate
protocol ChildDelegate: AnyObject { }

class ParentVC: UIViewController, ChildDelegate {
    var child: ChildVC?

    func showChild() {
        child = ChildVC()
        child?.delegate = self        // Parent → Child (strong)
    }                                 // Child → Parent (strong از طریق delegate)
}                                     // ⚠️ Retain cycle!

class ChildVC: UIViewController {
    var delegate: ChildDelegate?    // ❌ strong پیش‌فرض
}

// رفع: weak delegate
class ChildVC: UIViewController {
    weak var delegate: ChildDelegate? // ✅ weak — نگه نمی‌دارد
}

در این مثال، ParentVC از طریق ویژگی child یک strong reference به ChildVC نگه می‌دارد. ChildVC از طریق delegate یک strong reference به ParentVC نگه می‌دارد. چرخه بسته شده است. اصلاح: weak var delegate — reference retain count را افزایش نمی‌دهد و ParentVC می‌تواند آزاد شود.

NSTimer و retain cycle

NSTimer — یک منبع کلاسیک retain cycle است. تایمر target (معمولاً self) را نگه می‌دارد و target تایمر را از طریق ویژگی نگه می‌دارد. حتی اگر تایمر یکبار مصرف باشد، تا قبل از invalidate آزاد نمی‌شود. راه‌حل: همیشه timer.invalidate() را در deinit یا viewDidDisappear فراخوانی کنید.

معماری‌های لایه‌ای

در معماری‌های با مالکیت آبشاری (هماهنگ‌کننده‌ها، مسیریاب‌ها) اغلب چرخه‌های چندمرحله‌ای ایجاد می‌شود: Coordinator → ViewController → ViewModel → Coordinator (از طریق callback). هر strong reference در زنجیره باید آگاهانه انتخاب شود — یک weak reference در هر حلقه‌ای چرخه را می‌شکند.

Retain Cycle در بسته‌های Swift

بسته‌ها (closures) در Swift متغیرهای خارجی را با strong reference می‌گیرند. اگر بسته به عنوان ویژگی یک شیء ذخیره شود (مثلاً completion handler) و self را بگیرد، retain cycle ایجاد می‌شود: self → closure → self.

این شایع‌ترین منبع retain cycle در توسعه مدرن Swift است. به صورت ضمنی ایجاد می‌شود — توسعه‌دهنده ممکن است گرفتن self در بسته را متوجه نشود، به ویژه هنگام استفاده از نحو مختصر بدون self صریح.

swift
class DownloadService {
    var onComplete: ((Data) -> Void)?
    var result: Data?

    func startDownload() {
        // ❌ Retain cycle: self → onComplete → self
        onComplete = { data in
            self.result = data
            self.notifyUI()
        }

        // ✅ رفع: capture list با weak self
        onComplete = { [weak self] data in
            guard let self else { return }
            self.result = data
            self.notifyUI()
        }
    }

    func notifyUI() { }
}

Capture list [weak self] یک reference ضعیف به self در داخل بسته ایجاد می‌کند. اگر DownloadService قبل از اجرای بسته آزاد شود، self nil می‌شود و کد با خیال راحت از طریق guard خارج می‌شود. این الگوی استاندارد برای بسته‌های ناهمگام در Swift است — باید همیشه زمانی که بسته به عنوان ویژگی ذخیره می‌شود اعمال شود.

Unowned self در بسته‌ها

unowned self — جایگزینی برای weak self است، زمانی که self تضمیناً بیشتر از بسته عمر می‌کند. مثال: بسته همزمان که بلافاصله اجرا می‌شود (sorted, filter). در چنین مواردی self قطعاً زنده است و unowned ایمن است. با این حال unowned هنگام دسترسی به شیء آزاد شده crash می‌کند — بنابراین weak انتخاب ایمن پیش‌فرض محسوب می‌شود.

چگونه retain cycle را تشخیص دهیم: ابزارهای تشخیص

تشخیص retain cycle در مراحل اولیه برای عملکرد برنامه بسیار مهم است. ابزارها و روش‌های اصلی شناسایی ارجاعات چرخه‌ای در توسعه iOS را بررسی می‌کنیم.

Xcode Memory Debugger

Xcode Memory Debugger (Debug Memory Graph) — ابزار بصری که گراف اشیاء در حافظه را با ارجاعات آنها نشان می‌دهد. Retain cycle به صورت زنجیره بسته از فلش‌های strong نمایش داده می‌شود. برای اجرا: دکمه Debug Memory Graph را در پنل Debug area هنگام اجرای برنامه کلیک کنید. هر شیء با نوع، آدرس و لیست ارجاعات نشان داده می‌شود.

Instruments Leaks

Instruments Leaks — پروفایلری برای تشخیص خودکار نشت‌ها. تخصیص‌ها را ثبت کرده و گراف ارجاعات را در زمان واقعی تحلیل می‌کند. نه تنها retain cycle، بلکه ارجاعات فراموش شده، ViewControllerهای آزاد نشده و سایر نشت‌ها را نیز تشخیص می‌دهد. Leaks شیء دقیق و زنجیره نگهداری را نشان می‌دهد.

لاگ deinit

ساده‌ترین روش — اضافه کردن print در deinit هر کلاس کلیدی. اگر deinit هنگام نابودی مورد انتظار شیء فراخوانی نشود — retain cycle وجود دارد. این روش به ابزار نیاز ندارد و برای تشخیص اولیه مؤثر است.

ابزارنوعزمان استفاده
Memory Debuggerگراف بصریبررسی دستی پس از ناوبری
Instruments Leaksتحلیل خودکارتست رگرسیون، CI
deinit printلاگ دستیتوسعه، code review
Malloc Scribbleپرچم زمان اجرااشکال‌زدایی use-after-free

رویکرد توصیه شده: استفاده از deinit logging در مرحله توسعه، Memory Debugger — در تست دستی، Instruments Leaks — در pipeline CI/CD برای کنترل خودکار رگرسیون نشت‌ها.

پیشگیری از retain cycle و best practices

پیشگیری از retain cycle آسان‌تر از رفع آن در محیط تولید است. چند قانون که ریسک ارجاعات چرخه‌ای را به حداقل می‌رساند.

قانون weak delegate

همه delegateها و dataSourceها باید weak باشند. این قانون در UIKit تعبیه شده است: همه پروتکل‌های delegate در Apple SDK با ویژگی‌های weak اعلام شده‌اند (UITableView.delegate, UICollectionView.dataSource). برای پروتکل‌های خود از weak var delegate: MyDelegate? استفاده کنید و پروتکل را از AnyObject مشتق کنید.

Capture list در بسته‌ها

هر بسته‌ای که به عنوان ویژگی ذخیره می‌شود (completion handler, callback) و self را می‌گیرد، باید از [weak self] در capture list استفاده کند. استثنا — بسته‌هایی که بلافاصله اجرا می‌شوند و ذخیره نمی‌شوند (sorted, map, filter). برای آنها unowned self ایمن است.

بررسی معماری

در معماری‌های پیچیده (VIPER, Coordinators, Redux) جهت strong referenceها را ردیابی کنید. مالک یک strong reference به زیردست دارد، اما زیردست باید تنها از طریق weak یا unowned به مالک ارجاع دهد. جریان داده یک‌طرفه (unidirectional data flow) کنترل ارجاعات را ساده‌تر می‌کند.

swift
// مثال: بررسی با لاگ deinit
class BaseViewController: UIViewController {
    deinit {
        print("✅ \(type(of: self)) deallocated")
    }
}

// استفاده: همه ViewControllerها از BaseViewController ارث می‌برند
class ProfileVC: BaseViewController {
    var viewModel: ProfileViewModel?
    var onLogout: (() -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        onLogout = { [weak self] in
            self?.dismiss(animated: true)
        }
    }
}
// پس از بستن ProfileVC انتظار "✅ ProfileVC deallocated" در کنسول را داریم

کلاس پایه با لاگ deinit بازخورد فوری می‌دهد. اگر پیام در بسته شدن مورد انتظار صفحه ظاهر نشد — در این کلاس retain cycle وجود دارد. این روش را برای همه ViewControllerها به الگوی پروژه اضافه کنید.

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

تفاوت retain cycle با نشت حافظه در GC چیست؟

Retain cycle — مشکل خاص ARC است که در آن دایره بسته strong referenceها آزادسازی را مسدود می‌کند. در GC جمع‌آورنده دسترسی از ریشه (root set) را تحلیل می‌کند نه شمارنده مراجع را — بنابراین چرخه‌ها نشت محسوب نمی‌شوند. در ARC اما هر چرخه ایزوله‌ای یک نشت تضمینی است.

چگونه weak reference retain cycle را می‌شکند؟

Weak reference retain count شیء را افزایش نمی‌دهد. اگر یکی از strong referenceها را در چرخه با weak جایگزین کنید، retain count هر شیء می‌تواند صفر شود. پس از آزاد شدن شیء، weak reference به طور خودکار به nil تنظیم می‌شود و از دسترسی به حافظه مرده جلوگیری می‌کند.

آیا retain cycle می‌تواند از سه یا بیشتر شیء تشکیل شود؟

بله، retain cycle می‌تواند هر تعداد شیء را شامل شود: A → B → C → A. برای آزادسازی کافیست یک حلقه در چرخه شکسته شود — هر strong reference را با weak یا unowned جایگزین کنید. ابزارها کل گراف را نشان می‌دهند، نه فقط جفت اشیاء را.

چرا GCD DispatchWorkItem retain cycle ایجاد نمی‌کند؟

GCD (Grand Central Dispatch) بسته را پس از اجرا ذخیره نمی‌کند. DispatchWorkItem اجرا شده و آزاد می‌شود، حتی اگر بسته self را بگیرد. Retain cycle تنها زمانی ایجاد می‌شود که بسته به عنوان ویژگی ذخیره شود (completion handler در کلاس)، نه زمانی که به صف ارسال می‌شود.

چه نوع retain cycleهایی توسط Instruments تشخیص داده نمی‌شوند؟

Instruments Leaks همیشه retain cycleهای موقت (که چند ثانیه وجود دارند) و ارجاعات چرخه‌ای در اشیاء C/C++ از طریق bridge را پیدا نمی‌کند. برای بررسی کامل از Memory Debugger به صورت دستی + لاگ deinit همه اشیاء کلیدی صحنه استفاده کنید.

خلاصه

  • Retain Cycle — زنجیره بسته strong referenceها که آزادسازی اشیاء را در ARC مسدود می‌کند
  • علل — delegateها با strong reference، بسته‌هایی که self را می‌گیرند، روابط دوطرفه parent-child
  • راه‌حل — جایگزینی یک strong reference با weak یا unowned چرخه را می‌شکند
  • بسته‌ها — completion handlerهای ذخیره شده همیشه باید از [weak self] استفاده کنند
  • Delegateها — همیشه weak؛ پروتکل delegate باید از AnyObject مشتق شود
  • تشخیص — Xcode Memory Debugger, Instruments Leaks, لاگ deinit
  • پیشگیری — جریان داده یک‌طرفه, weak delegate, capture list, کلاس پایه با deinit

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

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

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

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