Retain Cycle — esensi, penyebab terjadinya dan penanganan dalam pengembangan aplikasi

Penulis: IT Sectr Diterbitkan: 2026-03-29 Waktu membaca: 8 mnt

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 — rantai tertutup referensi strong, di mana objek tidak dapat dilepaskan oleh ARC
  • Penyebab — dua (atau lebih) objek menyimpan referensi strong satu sama lain, penghilangan retain count tidak mungkin
  • Akibat — kebocoran memori: objek tetap di memori selamanya, konsumsi RAM meningkat
  • Solusi — mengganti salah satu referensi strong dalam siklus dengan weak atau unowned
  • Diagnostik — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

Apa itu Retain Cycle?

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.

Contoh retain cycle dalam pengembangan iOS

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.

Parent-Child dengan delegasi

Skenario klasik: objek induk (misalnya UIViewController) membuat objek anak dan menjadi delegasinya. Jika keduanya menggunakan referensi strong, terjadilah retain cycle. Solusi — delegasi harus weak.

swift
// 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 dan retain cycle

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.

Arsitektur berlapis

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.

Retain Cycle dalam closure Swift

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.

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()
        }

        // ✅ 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 dalam closure

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.

Cara mendeteksi retain cycle: alat diagnostik

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

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

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.

Logging deinit

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.

AlatTipeKapan digunakan
Memory DebuggerGraf visualPemeriksaan manual setelah navigasi
Instruments LeaksAnalisis otomatisPengujian regresi, CI
deinit printLogging manualPengembangan, code review
Malloc ScribbleBendera runtimeDebug 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.

Pencegahan retain cycle dan best practices

Mencegah retain cycle lebih mudah daripada memperbaikinya di produksi. Beberapa aturan yang meminimalkan risiko referensi siklik.

Aturan weak delegate

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.

Capture list dalam closure

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.

Memeriksa arsitektur

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.

swift
// 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

Apa perbedaan retain cycle dengan kebocoran memori di GC?

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.

Bagaimana weak reference memutus retain cycle?

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.

Bisakah retain cycle terdiri dari tiga objek atau lebih?

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.

Mengapa GCD DispatchWorkItem tidak membuat retain cycle?

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.

Jenis retain cycle apa yang tidak terdeteksi oleh Instruments?

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

  • Retain Cycle — rantai tertutup referensi strong yang memblokir pelepasan objek di ARC
  • Penyebab — delegasi dengan referensi strong, closure yang menangkap self, hubungan parent-child dua arah
  • Solusi — mengganti satu referensi strong dengan weak atau unowned memutus siklus
  • Closure — completion handler yang disimpan harus selalu menggunakan [weak self]
  • Delegasi — selalu weak; protokol delegasi harus mewarisi AnyObject
  • Diagnostik — Xcode Memory Debugger, Instruments Leaks, logging deinit
  • Pencegahan — aliran data searah, weak delegate, capture list, kelas dasar dengan deinit

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.

Diskusikan proyek

Baca juga