Retain Cycle (referensi siklik) — situasi dalam ARC ketika dua atau lebih objek saling merujuk melalui referensi strong, membentuk siklus tertutup. Menurut Apple Memory Management Guide, 2026, retain cycle memblokir pelepasan semua objek dalam siklus, karena setiap objek memiliki retain count ≥ 1. Berbeda dengan kebocoran memori di GC, retain cycle secara terjamin menjaga objek tetap hidup selama setidaknya satu peserta eksternal siklus masih hidup — dan bahkan setelah kehilangan semua referensi eksternal, jika siklus terisolasi.
Poin Penting
Retain Cycle — adalah situasi di mana dua atau lebih objek saling memiliki melalui referensi strong, menciptakan graf ketergantungan tertutup. ARC tidak dapat melepaskan objek mana pun karena retain count masing-masing selalu ≥ 1: objek A memegang B, B memegang A, dan penghitungnya tidak pernah menjadi nol.
Masalah ini hanya terjadi pada sistem dengan penghitungan referensi (ARC, MRR). Dalam Garbage Collection, pengumpul menentukan ketidakcapaian berdasarkan graf referensi dari root set — siklus bukanlah hambatan. Di ARC, siklus setara dengan kebocoran, karena pelepasan deterministik berdasarkan penghitung tidak dapat menyelesaikan ketergantungan melingkar.
Menurut WWDC 2012 Session 406, retain cycle adalah penyebab paling umum kebocoran memori di aplikasi Objective-C dan Swift. Skenario tipikal: hubungan parent-child dengan delegasi, closure yang menangkap self, dan arsitektur berlapis dengan koneksi dua arah.
Mari kita lihat skenario klasik retain cycle yang dihadapi setiap pengembang iOS. Memahami pola-pola ini adalah dasar untuk menulis kode yang aman dengan ARC.
Skenario klasik: objek induk (misalnya UIViewController) membuat objek anak dan menjadi delegasinya. Jika keduanya menggunakan referensi strong, terjadilah retain cycle. Solusi — delegasi harus weak.
// KESALAHAN: retain cycle melalui 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 melalui delegate)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong secara default
}
// PERBAIKAN: weak delegate
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — tidak menahan
}
Dalam contoh, ParentVC menyimpan referensi strong ke ChildVC melalui properti child. ChildVC menyimpan referensi strong ke ParentVC melalui delegate. Siklus tertutup. Perbaikan: weak var delegate — referensi tidak meningkatkan retain count, dan ParentVC dapat dilepaskan.
NSTimer — sumber klasik retain cycle. Timer mempertahankan target (biasanya self), dan target mempertahankan timer melalui properti. Bahkan jika timer sekali pakai, ia tidak akan dilepaskan sampai invalidate. Solusi: selalu panggil timer.invalidate() di deinit atau viewDidDisappear.
Dalam arsitektur dengan kepemilikan bertingkat (koordinator, router) sering muncul siklus multi-langkah: Coordinator → ViewController → ViewModel → Coordinator (melalui callback). Setiap referensi strong dalam rantai harus dipilih secara sadar — satu referensi weak di mata rantai mana pun memutus siklus.
Closure di Swift menangkap variabel eksternal melalui referensi strong. Jika closure disimpan sebagai properti objek (misalnya completion handler) dan menangkap self, terbentuklah retain cycle: self → closure → self.
Ini adalah sumber retain cycle paling umum dalam pengembangan Swift modern. Ia muncul secara implisit — pengembang mungkin tidak menyadari penangkapan self dalam closure, terutama saat menggunakan sintaks singkat tanpa self eksplisit.
class DownloadService {
var onComplete: ((Data) -> Void)?
var result: Data?
func startDownload() {
// ❌ Retain cycle: self → onComplete → self
onComplete = { data in
self.result = data
self.notifyUI()
}
// ✅ Perbaikan: capture list dengan weak self
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
Capture list [weak self] membuat referensi lemah ke self di dalam closure. Jika DownloadService dilepaskan sebelum closure dijalankan, self menjadi nil dan kode keluar dengan aman melalui guard. Ini adalah pola standar untuk closure asinkron di Swift — harus selalu diterapkan ketika closure disimpan sebagai properti.
unowned self — alternatif untuk weak self, ketika self dijamin hidup lebih lama dari closure. Contoh: closure sinkron yang dijalankan segera (sorted, filter). Dalam kasus seperti itu, self pasti hidup dan unowned aman. Namun unowned akan crash saat mengakses objek yang sudah dilepaskan — oleh karena itu weak dianggap sebagai pilihan aman secara default.
Deteksi dini retain cycle sangat penting untuk kinerja aplikasi. Mari kita lihat alat dan metode utama untuk mengidentifikasi referensi siklik dalam pengembangan iOS.
Xcode Memory Debugger (Debug Memory Graph) — alat visual yang menunjukkan graf objek dalam memori dengan referensinya. Retain cycle ditampilkan sebagai rantai tertutup panah strong. Untuk menjalankan: klik tombol Debug Memory Graph di panel Debug area saat aplikasi berjalan. Setiap objek ditampilkan dengan tipe, alamat, dan daftar referensi.
Instruments Leaks — profiler untuk deteksi kebocoran otomatis. Mencatat alokasi dan menganalisis graf referensi secara real-time. Mendeteksi tidak hanya retain cycle, tetapi juga referensi yang terlupakan, ViewController yang tidak dilepaskan, dan kebocoran lainnya. Leaks menunjukkan objek yang tepat dan rantai retensi.
Cara paling sederhana — tambahkan print di deinit setiap kelas kunci. Jika deinit tidak dipanggil saat penghancuran objek yang diharapkan — ada retain cycle. Metode ini tidak memerlukan alat dan efektif untuk diagnostik awal.
| Alat | Tipe | Kapan digunakan |
|---|---|---|
| Memory Debugger | Graf visual | Pemeriksaan manual setelah navigasi |
| Instruments Leaks | Analisis otomatis | Pengujian regresi, CI |
| deinit print | Logging manual | Pengembangan, code review |
| Malloc Scribble | Bendera runtime | Debug use-after-free |
Pendekatan yang direkomendasikan: gunakan logging deinit pada tahap pengembangan, Memory Debugger — saat pengujian manual, Instruments Leaks — di pipeline CI/CD untuk kontrol regresi kebocoran otomatis.
Mencegah retain cycle lebih mudah daripada memperbaikinya di produksi. Beberapa aturan yang meminimalkan risiko referensi siklik.
Semua delegasi dan dataSource harus weak. Aturan ini sudah tertanam di UIKit: semua protokol delegasi di Apple SDK dideklarasikan dengan properti weak (UITableView.delegate, UICollectionView.dataSource). Untuk protokol Anda sendiri, gunakan weak var delegate: MyDelegate? dan warisi protokol dari AnyObject.
Setiap closure yang disimpan sebagai properti (completion handler, callback) dan menangkap self harus menggunakan [weak self] di capture list. Pengecualian — closure yang dijalankan segera dan tidak disimpan (sorted, map, filter). Untuk itu, unowned self aman.
Dalam arsitektur kompleks (VIPER, Coordinators, Redux) pantau arah referensi strong. Pemilik menyimpan referensi strong ke bawahan, tetapi bawahan harus merujuk ke pemilik hanya melalui weak atau unowned. Aliran data searah (unidirectional data flow) menyederhanakan kontrol referensi.
// Contoh: pemeriksaan dengan logging deinit
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// Penggunaan: semua ViewController mewarisi BaseViewController
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// Saat menutup ProfileVC diharapkan „✅ ProfileVC deallocated” di konsol
Kelas dasar dengan logging deinit memberikan umpan balik instan. Jika pesan tidak muncul saat penutupan layar yang diharapkan — di kelas ini terdapat retain cycle. Tambahkan praktik ini ke template proyek untuk semua ViewController.
Pertanyaan yang Sering Diajukan
Retain cycle — masalah spesifik ARC di mana lingkaran tertutup referensi strong memblokir pelepasan. Di GC, pengumpul menganalisis ketercapaian dari root set, bukan penghitung referensi — oleh karena itu siklus bukanlah kebocoran. Di ARC, siklus terisolasi mana pun adalah kebocoran terjamin.
Weak reference tidak meningkatkan retain count objek. Jika Anda mengganti salah satu referensi strong dalam siklus dengan weak, retain count setiap objek dapat menjadi nol. Setelah objek dilepaskan, weak reference secara otomatis diatur ke nil, mencegah akses ke memori mati.
Ya, retain cycle dapat mencakup jumlah objek berapa pun: A → B → C → A. Untuk melepaskan, cukup putuskan satu mata rantai dalam siklus — ganti referensi strong mana pun dengan weak atau unowned. Alat menunjukkan seluruh graf, bukan hanya pasangan objek.
GCD (Grand Central Dispatch) tidak menyimpan closure setelah eksekusi. DispatchWorkItem dijalankan dan dilepaskan, bahkan jika closure menangkap self. Retain cycle hanya terjadi ketika closure disimpan sebagai properti (completion handler di kelas), bukan ketika dikirim ke antrian.
Instruments Leaks tidak selalu menemukan retain cycle sementara (yang ada selama beberapa detik) dan referensi siklik di objek C/C++ melalui bridge. Untuk pemeriksaan menyeluruh, gunakan Memory Debugger manual + logging deinit semua objek kunci dalam scene.
Ringkasan
Kami akan mengembangkan aplikasi seluler turnkey
IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.
Baca juga