Retain Cycle (ارجاع چرخهای) — وضعیتی در ARC که دو یا چند شیء از طریق strong referenceها به یکدیگر ارجاع میدهند و یک چرخه بسته تشکیل میدهند. بر اساس Apple Memory Management Guide, 2026، retain cycle آزادسازی همه اشیاء در چرخه را مسدود میکند، زیرا هر یک retain count ≥ 1 دارند. بر خلاف نشت حافظه در GC، 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 با آن مواجه میشود بررسی میکنیم. درک این الگوها اساس نوشتن کد امن با ARC است.
سناریوی کلاسیک: شیء والد (مثلاً UIViewController) یک شیء فرزند ایجاد میکند و delegate آن میشود. اگر هر دو از strong reference استفاده کنند، retain cycle ایجاد میشود. راهحل — delegate باید weak باشد.
// خطا: 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 است. تایمر target (معمولاً self) را نگه میدارد و target تایمر را از طریق ویژگی نگه میدارد. حتی اگر تایمر یکبار مصرف باشد، تا قبل از invalidate آزاد نمیشود. راهحل: همیشه timer.invalidate() را در deinit یا viewDidDisappear فراخوانی کنید.
در معماریهای با مالکیت آبشاری (هماهنگکنندهها، مسیریابها) اغلب چرخههای چندمرحلهای ایجاد میشود: Coordinator → ViewController → ViewModel → Coordinator (از طریق callback). هر strong reference در زنجیره باید آگاهانه انتخاب شود — یک weak reference در هر حلقهای چرخه را میشکند.
بستهها (closures) در Swift متغیرهای خارجی را با strong reference میگیرند. اگر بسته به عنوان ویژگی یک شیء ذخیره شود (مثلاً completion handler) و self را بگیرد، retain cycle ایجاد میشود: self → closure → self.
این شایعترین منبع retain cycle در توسعه مدرن Swift است. به صورت ضمنی ایجاد میشود — توسعهدهنده ممکن است گرفتن self در بسته را متوجه نشود، به ویژه هنگام استفاده از نحو مختصر بدون self صریح.
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 — جایگزینی برای weak self است، زمانی که self تضمیناً بیشتر از بسته عمر میکند. مثال: بسته همزمان که بلافاصله اجرا میشود (sorted, filter). در چنین مواردی self قطعاً زنده است و unowned ایمن است. با این حال unowned هنگام دسترسی به شیء آزاد شده crash میکند — بنابراین weak انتخاب ایمن پیشفرض محسوب میشود.
تشخیص retain cycle در مراحل اولیه برای عملکرد برنامه بسیار مهم است. ابزارها و روشهای اصلی شناسایی ارجاعات چرخهای در توسعه iOS را بررسی میکنیم.
Xcode Memory Debugger (Debug Memory Graph) — ابزار بصری که گراف اشیاء در حافظه را با ارجاعات آنها نشان میدهد. Retain cycle به صورت زنجیره بسته از فلشهای strong نمایش داده میشود. برای اجرا: دکمه Debug Memory Graph را در پنل Debug area هنگام اجرای برنامه کلیک کنید. هر شیء با نوع، آدرس و لیست ارجاعات نشان داده میشود.
Instruments Leaks — پروفایلری برای تشخیص خودکار نشتها. تخصیصها را ثبت کرده و گراف ارجاعات را در زمان واقعی تحلیل میکند. نه تنها retain cycle، بلکه ارجاعات فراموش شده، ViewControllerهای آزاد نشده و سایر نشتها را نیز تشخیص میدهد. Leaks شیء دقیق و زنجیره نگهداری را نشان میدهد.
سادهترین روش — اضافه کردن 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 آسانتر از رفع آن در محیط تولید است. چند قانون که ریسک ارجاعات چرخهای را به حداقل میرساند.
همه delegateها و dataSourceها باید weak باشند. این قانون در UIKit تعبیه شده است: همه پروتکلهای delegate در Apple SDK با ویژگیهای weak اعلام شدهاند (UITableView.delegate, UICollectionView.dataSource). برای پروتکلهای خود از weak var delegate: MyDelegate? استفاده کنید و پروتکل را از AnyObject مشتق کنید.
هر بستهای که به عنوان ویژگی ذخیره میشود (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) کنترل ارجاعات را سادهتر میکند.
// مثال: بررسی با لاگ 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 — مشکل خاص ARC است که در آن دایره بسته strong referenceها آزادسازی را مسدود میکند. در GC جمعآورنده دسترسی از ریشه (root set) را تحلیل میکند نه شمارنده مراجع را — بنابراین چرخهها نشت محسوب نمیشوند. در ARC اما هر چرخه ایزولهای یک نشت تضمینی است.
Weak reference retain count شیء را افزایش نمیدهد. اگر یکی از strong referenceها را در چرخه با weak جایگزین کنید، retain count هر شیء میتواند صفر شود. پس از آزاد شدن شیء، weak reference به طور خودکار به nil تنظیم میشود و از دسترسی به حافظه مرده جلوگیری میکند.
بله، retain cycle میتواند هر تعداد شیء را شامل شود: A → B → C → A. برای آزادسازی کافیست یک حلقه در چرخه شکسته شود — هر strong reference را با weak یا unowned جایگزین کنید. ابزارها کل گراف را نشان میدهند، نه فقط جفت اشیاء را.
GCD (Grand Central Dispatch) بسته را پس از اجرا ذخیره نمیکند. DispatchWorkItem اجرا شده و آزاد میشود، حتی اگر بسته self را بگیرد. Retain cycle تنها زمانی ایجاد میشود که بسته به عنوان ویژگی ذخیره شود (completion handler در کلاس)، نه زمانی که به صف ارسال میشود.
Instruments Leaks همیشه retain cycleهای موقت (که چند ثانیه وجود دارند) و ارجاعات چرخهای در اشیاء C/C++ از طریق bridge را پیدا نمیکند. برای بررسی کامل از Memory Debugger به صورت دستی + لاگ deinit همه اشیاء کلیدی صحنه استفاده کنید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید