Retain Cycle — ουσία, αιτίες εμφάνισης και εξάλειψη στην ανάπτυξη εφαρμογών

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-03-29 Χρόνος ανάγνωσης: 8 λεπ

Retain Cycle (κυκλική αναφορά) — κατάσταση στο ARC όταν δύο ή περισσότερα αντικείμενα αναφέρονται το ένα στο άλλο μέσω strong αναφορών, σχηματίζοντας έναν κλειστό κύκλο. Σύμφωνα με τον Apple Memory Management Guide, 2026, το retain cycle μπλοκάρει την απελευθέρωση όλων των αντικειμένων στον κύκλο, καθώς το καθένα έχει retain count ≥ 1. Σε αντίθεση με μια διαρροή μνήμης στο GC, το retain cycle κρατά εγγυημένα τα αντικείμενα ζωντανά όσο ζει τουλάχιστον ένας εξωτερικός συμμετέχων του κύκλου — και ακόμα και μετά την απώλεια όλων των εξωτερικών αναφορών, εάν ο κύκλος είναι απομονωμένος.

Κύρια σημεία

  • Retain Cycle — κλειστή αλυσίδα strong αναφορών, όπου τα αντικείμενα δεν μπορούν να απελευθερωθούν από το ARC
  • Αιτία — δύο (ή περισσότερα) αντικείμενα διατηρούν strong αναφορά το ένα στο άλλο, μηδενισμός του retain count είναι αδύνατος
  • Συνέπειες — διαρροή μνήμης: τα αντικείμενα παραμένουν στη μνήμη για πάντα, αυξάνεται η κατανάλωση RAM
  • Λύση — αντικατάσταση μιας από τις strong αναφορές στον κύκλο με weak ή unowned
  • Διάγνωση — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

Τι είναι το Retain Cycle;

Retain Cycle — είναι μια κατάσταση όπου δύο ή περισσότερα αντικείμενα κατέχουν το ένα το άλλο μέσω strong αναφορών, δημιουργώντας έναν κλειστό γράφο εξάρτησης. Το 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 με αντιπροσώπους (delegates), closures που δεσμεύουν το self και πολυεπίπεδες αρχιτεκτονικές με αμφίδρομες συνδέσεις.

Παραδείγματα retain cycle στην ανάπτυξη iOS

Ας εξετάσουμε τα κλασικά σενάρια retain cycle που αντιμετωπίζει κάθε προγραμματιστής iOS. Η κατανόηση αυτών των προτύπων είναι η βάση για τη σύνταξη ασφαλούς κώδικα με ARC.

Parent-Child με αντιπρόσωπο

Κλασικό σενάριο: το γονικό αντικείμενο (π.χ. UIViewController) δημιουργεί ένα θυγατρικό αντικείμενο και γίνεται αντιπρόσωπός του. Εάν και τα δύο χρησιμοποιούν strong αναφορές, δημιουργείται retain cycle. Λύση — ο αντιπρόσωπος πρέπει να είναι 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 κρατά strong αναφορά στο ChildVC μέσω της ιδιότητας child. Το ChildVC κρατά strong αναφορά στο ParentVC μέσω του delegate. Ο κύκλος είναι κλειστός. Διόρθωση: weak var delegate — η αναφορά δεν αυξάνει το retain count, και το ParentVC μπορεί να απελευθερωθεί.

NSTimer και retain cycle

NSTimer — μια κλασική πηγή retain cycle. Ο χρονοδιακόπτης κρατά τον στόχο (συνήθως self), και ο στόχος κρατά τον χρονοδιακόπτη μέσω μιας ιδιότητας. Ακόμα κι αν ο χρονοδιακόπτης είναι μίας χρήσης, δεν απελευθερώνεται μέχρι το invalidate. Λύση: καλείτε πάντα το timer.invalidate() στο deinit ή στο viewDidDisappear.

Πολυεπίπεδες αρχιτεκτονικές

Σε αρχιτεκτονικές με ιδιοκτησία καταρράκτη (συντονιστές, δρομολογητές) συχνά δημιουργούνται πολυσταδιακοί κύκλοι: Coordinator → ViewController → ViewModel → Coordinator (μέσω callback). Κάθε strong αναφορά στην αλυσίδα πρέπει να επιλέγεται συνειδητά — μία weak αναφορά σε οποιονδήποτε κρίκο διακόπτει τον κύκλο.

Retain Cycle στα closures Swift

Τα closures στη Swift δεσμεύουν εξωτερικές μεταβλητές μέσω strong αναφοράς. Εάν ένα closure αποθηκεύεται ως ιδιότητα ενός αντικειμένου (π.χ. completion handler) και δεσμεύει το self, σχηματίζεται retain cycle: self → closure → self.

Αυτή είναι η πιο συνηθισμένη πηγή retain cycle στη σύγχρονη ανάπτυξη Swift. Δημιουργείται σιωπηρά — ο προγραμματιστής μπορεί να μην παρατηρήσει τη δέσμευση του self στο closure, ειδικά όταν χρησιμοποιεί συντομευμένη σύνταξη χωρίς ρητό 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] δημιουργεί μια ασθενή αναφορά στο self εντός του closure. Εάν το DownloadService απελευθερωθεί πριν από την εκτέλεση του closure, το self γίνεται nil και ο κώδικας εξέρχεται με ασφάλεια μέσω του guard. Αυτό είναι το τυπικό πρότυπο για ασύγχρονα closures στη Swift — πρέπει να εφαρμόζεται πάντα όταν το closure αποθηκεύεται ως ιδιότητα.

Unowned self στα closures

unowned self — εναλλακτική του weak self, όταν το self είναι εγγυημένο ότι ζει περισσότερο από το closure. Παράδειγμα: σύγχρονο closure που εκτελείται άμεσα (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 στη φάση ανάπτυξης, Memory Debugger — στον χειροκίνητο έλεγχο, Instruments Leaks — στο CI/CD pipeline για αυτόματο έλεγχο παλινδρόμησης διαρροών.

Πρόληψη retain cycle και best practices

Η πρόληψη του retain cycle είναι ευκολότερη από την επιδιόρθωσή του στην παραγωγή. Μερικοί κανόνες που μειώνουν τον κίνδυνο κυκλικών αναφορών στο ελάχιστο.

Ο κανόνας weak delegate

Όλοι οι αντιπρόσωποι και τα dataSource πρέπει να είναι weak. Αυτός ο κανόνας είναι ενσωματωμένος στο UIKit: όλα τα πρωτόκολλα αντιπροσώπων στο Apple SDK είναι δηλωμένα με weak ιδιότητες (UITableView.delegate, UICollectionView.dataSource). Για τα δικά σας πρωτόκολλα, χρησιμοποιήστε weak var delegate: MyDelegate? και κληρονομήστε το πρωτόκολλο από το AnyObject.

Capture list στα closures

Κάθε closure που αποθηκεύεται ως ιδιότητα (completion handler, callback) και δεσμεύει το self πρέπει να χρησιμοποιεί [weak self] στη capture list. Εξαίρεση — closures που εκτελούνται άμεσα και δεν αποθηκεύονται (sorted, map, filter). Γι' αυτά, το unowned self είναι ασφαλές.

Έλεγχος αρχιτεκτονικής

Σε σύνθετες αρχιτεκτονικές (VIPER, Coordinators, Redux) παρακολουθείτε την κατεύθυνση των strong αναφορών. Ο κάτοχος κρατά strong αναφορά στον υφιστάμενο, αλλά ο υφιστάμενος πρέπει να αναφέρεται στον κάτοχο μόνο μέσω 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 αναφορών μπλοκάρει την απελευθέρωση. Στο GC, ο συλλέκτης αναλύει την προσβασιμότητα από τη ρίζα (root set), όχι τον μετρητή αναφορών — επομένως οι κύκλοι δεν αποτελούν διαρροή. Στο ARC όμως, οποιοσδήποτε απομονωμένος κύκλος είναι εγγυημένη διαρροή.

Πώς μια weak αναφορά διακόπτει το retain cycle;

Η Weak αναφορά δεν αυξάνει το retain count του αντικειμένου. Εάν αντικαταστήσετε μία από τις strong αναφορές στον κύκλο με weak, το retain count κάθε αντικειμένου μπορεί να μηδενιστεί. Μετά την απελευθέρωση του αντικειμένου, η weak αναφορά τίθεται αυτόματα σε nil, αποτρέποντας την πρόσβαση σε νεκρή μνήμη.

Μπορεί ένα retain cycle να αποτελείται από τρία ή περισσότερα αντικείμενα;

Ναι, ένα retain cycle μπορεί να περιλαμβάνει οποιονδήποτε αριθμό αντικειμένων: A → B → C → A. Για απελευθέρωση, αρκεί να σπάσετε έναν κρίκο στον κύκλο — αντικαταστήστε οποιαδήποτε strong αναφορά με weak ή unowned. Τα εργαλεία δείχνουν ολόκληρο τον γράφο, όχι μόνο ζεύγη αντικειμένων.

Γιατί το GCD DispatchWorkItem δεν δημιουργεί retain cycle;

Το GCD (Grand Central Dispatch) δεν αποθηκεύει το closure μετά την εκτέλεση. Το DispatchWorkItem εκτελείται και απελευθερώνεται, ακόμα κι αν το closure δεσμεύει το self. Retain cycle δημιουργείται μόνο όταν το closure αποθηκεύεται ως ιδιότητα (completion handler σε μια κλάση), όχι όταν μεταβιβάζεται σε μια ουρά.

Ποιοι τύποι retain cycle δεν ανιχνεύονται από το Instruments;

Το Instruments Leaks δεν βρίσκει πάντα προσωρινά retain cycle (που υπάρχουν για δευτερόλεπτα) και κυκλικές αναφορές σε αντικείμενα C/C++ μέσω bridge. Για πλήρη έλεγχο, χρησιμοποιήστε χειροκίνητα το Memory Debugger + καταγραφή deinit όλων των βασικών αντικειμένων στη σκηνή.

Σύνοψη

  • Retain Cycle — κλειστή αλυσίδα strong αναφορών που μπλοκάρει την απελευθέρωση αντικειμένων στο ARC
  • Αιτίες — αντιπρόσωποι με strong αναφορά, closures που δεσμεύουν self, αμφίδρομες σχέσεις parent-child
  • Λύση — αντικατάσταση μιας strong αναφοράς με weak ή unowned σπάει τον κύκλο
  • Closures — τα αποθηκευμένα completion handler πρέπει πάντα να χρησιμοποιούν [weak self]
  • Αντιπρόσωποι — πάντα weak; το πρωτόκολλο αντιπροσώπου πρέπει να κληρονομεί AnyObject
  • Διάγνωση — Xcode Memory Debugger, Instruments Leaks, καταγραφή deinit
  • Πρόληψη — μονόδρομη ροή δεδομένων, weak delegate, capture list, βασική κλάση με deinit

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης