Διαρροή Μνήμης σε Κινητές Εφαρμογές — τι είναι, αιτίες και μέθοδοι ανίχνευσης

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

Διαρροή μνήμης (Memory Leak) — κατάσταση όπου η εφαρμογή διατηρεί αναφορές σε αντικείμενα που δεν χρειάζονται πλέον, εμποδίζοντας τον garbage collector να απελευθερώσει την occupied memory. Σύμφωνα με τα δεδομένα του LeakCanary, ακόμη και σε καλογραμμένες εφαρμογές εμφανίζονται 3–5 διαρροές ανά 10.000 γραμμές κώδικα. Κάθε διαρροή σταδιακά μειώνει τη διαθέσιμη μνήμη, οδηγώντας σε επιβράδυνση και OutOfMemoryError.

Κύρια Σημεία

  • Memory Leak — το αντικείμενο παραμένει στη μνήμη, αν και δεν υπάρχουν ενεργές αναφορές από τη λογική της εφαρμογής
  • Στατικές αναφορές σε Activity ή Context — η πιο συχνή αιτία διαρροών σε Android
  • LeakCanary — το τυπικό εργαλείο για αυτόματη ανίχνευση διαρροών σε Android
  • WeakReference και Application Context — βασικές τεχνικές πρόληψης διαρροών
  • Lifecycle-aware components εξαλείφουν μια ολόκληρη κατηγορία διαρροών που σχετίζονται με συνδρομές

Τι είναι διαρροή μνήμης

Διαρροή μνήμης (Memory Leak) — κατάσταση κατά την οποία ένα αντικείμενο παραμένει προσβάσιμο μέσω μιας αλυσίδας ισχυρών αναφορών (Strong Reference), αν και λογικά δεν χρειάζεται πλέον από την εφαρμογή. Ο garbage collector (GC) θεωρεί ένα τέτοιο αντικείμενο ζωντανό και δεν ελευθερώνει τη μνήμη που καταλαμβάνει. Ως αποτέλεσμα, η διαθέσιμη μνήμη heap μειώνεται συνεχώς και η συχνότητα των παύσεων GC αυξάνεται.

Σε αντίθεση με γλώσσες με χειροκίνητη διαχείριση μνήμης (C, C++), σε Java/Kotlin η διαρροή δεν είναι μια ξεχασμένη free(), αλλά μια ξεχασμένη αναφορά. Όσο υπάρχει strong reference από το αντικείμενο ρίζας (GC Root) προς το αντικείμενο που παρουσιάζει διαρροή, το GC το θεωρεί απαραίτητο. Τυπικά GC Root: στατικά πεδία, ενεργά νήματα, στοίβα κλήσεων, JNI-καθολικές αναφορές.

Ο κίνδυνος των διαρροών είναι το σωρευτικό τους αποτέλεσμα. Μία διαρροή 100 KB δεν είναι αισθητή, αλλά 100 τέτοιες διαρροές καταλαμβάνουν 10 MB και η εφαρμογή αρχίζει να επιβραδύνεται λόγω συχνών GC. Η κρίσιμη μάζα διαρροών οδηγεί σε OutOfMemoryError και κατάρρευση της εφαρμογής. Συμπτώματα διαρροής: σταθερή αύξηση κατανάλωσης μνήμης στο γράφημα Profiler, συχνές παύσεις GC με STW (Stop The World) και μείωση απόδοσης UI.

Συνηθισμένοι τύποι διαρροών σε κινητές εφαρμογές

Πέντε τύποι διαρροών καλύπτουν το 95% των περιπτώσεων στην κινητή ανάπτυξη. Κάθε ένας έχει τη δική του αιτία και χαρακτηριστικό μοτίβο στον κώδικα.

Στατική αναφορά σε Activity ή Context

Η πιο γνωστή διαρροή σε Android — αποθήκευση στατικής αναφοράς σε Activity ή Context. Τυπικός κώδικας: ένα στατικό πεδίο Activity που δεν μηδενίζεται στο onDestroy(). Όσο το στατικό πεδίο ζει, ολόκληρο το Activity με το δέντρο View του, το οποίο μπορεί να καταλαμβάνει 1–10 MB, παραμένει ζωντανό. Αυτή είναι μια κλασική διαρροή που το LeakCanary βρίσκει πρώτα.

Λύση: ποτέ μην αποθηκεύετε Activity ή Context σε στατικά πεδία. Χρησιμοποιήστε Application Context για singleton που επιβιώνουν του Activity. Αν χρειάζεστε αναφορά σε Activity — χρησιμοποιήστε WeakReference<Activity>.

kotlin
object MySingleton {
    private var weakActivity: WeakReference<Activity>? = null

    fun attach(activity: Activity) {
        weakActivity = WeakReference(activity)
    }
}

Εσωτερικές κλάσεις με σιωπηρή αναφορά

Ανώνυμες κλάσεις και μη-στατικές ένθετες κλάσεις σιωπηρά διατηρούν αναφορά στην κλάση που τις περιέχει. Ένα Runnable που μεταδίδεται σε Handler και εκτελείται μετά το onDestroy() κρατά ολόκληρο το Activity. Ένα callback Retrofit που κλείνει το Activity κάνει το ίδιο. Αυτός είναι ο πιο ύπουλος τύπος διαρροής — η σιωπηρή αναφορά δεν είναι ορατή στον κώδικα.

Οι εκφράσεις object και τα lambda σε Kotlin επίσης συλλαμβάνουν αναφορές στην εξωτερική κλάση. Κάντε τις ένθετες κλάσεις στατικές (ή top-level σε Kotlin) και μεταδώστε εξωτερικές αναφορές μέσω WeakReference. Για lambda, χρησιμοποιήστε την προσέγγιση Lifecycle-aware με viewLifecycleOwner.

Απεγγραφείς ακροατές και συνδρομές

Εγγραφή σε υπηρεσίες συστήματος χωρίς απεγγραφή — άμεση διαρροή. SensorManager, LocationManager, NotificationListener που καταχωρούνται σε onResume() χωρίς κλήση unregister σε onPause() κρατούν το Activity. Παρόμοια: RxJava Disposable που δεν προστίθεται σε CompositeDisposable και coroutine που εκκινείται μέσω GlobalScope.

Χρησιμοποιήστε Lifecycle-aware components: το observe() με LifecycleOwner κάνει αυτόματα απεγγραφή σε onDestroy(). Για RxJava — viewLifecycleOwner.lifecycle.addObserver με DisposableObserver. Για coroutines — το lifecycleScope.launch() είναι δεμένο με τον κύκλο ζωής.

kotlin
// αυτόματη απεγγραφή μέσω Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// coroutines με lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap χωρίς recycle

Bitmap καταλαμβάνει σημαντικό ποσό μνήμης heap: ένα FullHD-bitmap — 1920 × 1080 × 4 bytes = 8.3 MB. Αν δημιουργείται Bitmap για κάθε στοιχείο λίστας και δεν καλείται recycle() κατά την απόκρυψη, η μνήμη εξαντλείται γρήγορα. Σε παλιές εκδόσεις Android (πριν από 3.0), το Bitmap αποθηκευόταν στη native μνήμη, αλλά σε σύγχρονες — στο heap Dalvik/ART, και ο GC μπορεί να το ελευθερώσει μόνο αν δεν υπάρχει strong reference.

Χρησιμοποιήστε Glide ή Coil για φόρτωση εικόνων — αυτές οι βιβλιοθήκες διαχειρίζονται αυτόματα την προσωρινή αποθήκευση και το recycle. Αν εργάζεστε απευθείας με Bitmap, καλέστε bitmap.recycle() για μεγάλες εικόνες που δεν εμφανίζονται πλέον και χρησιμοποιήστε inSampleSize για φόρτωση μικρογραφιών.

Αναφορά Fragment μετά από onDestroyView

Fragment έχει δύο κύκλους ζωής: του ίδιου του Fragment και του View του. Μετά από onDestroyView() το δέντρο View καταστρέφεται, αλλά το ίδιο το Fragment μπορεί να παραμείνει στη μνήμη αν υπάρχει εξωτερική αναφορά. Τυπικό σφάλμα — αποθήκευση αναφοράς σε Fragment σε adapter ViewPager ή σε γράφο πλοήγησης που δεν καθαρίζεται κατά την καταστροφή.

Ποτέ μην αποθηκεύετε αναφορά σε Fragment σε πεδία μακρόβιων αντικειμένων. Χρησιμοποιήστε childFragmentManager για ένθετα fragments και observe() με LifecycleOwner για μεταφορά δεδομένων μεταξύ τους. Το ViewPager2 έλυσε αυτό το πρόβλημα σε επίπεδο API: το FragmentTransactionAdapter διαχειρίζεται σωστά τον κύκλο ζωής.

Πώς να ανιχνεύσετε διαρροή μνήμης

Ανίχνευση διαρροής απαιτεί έλεγχο δύο γεγονότων: η μνήμη δεν επιστρέφει μετά το expected lifetime και ο αριθμός αντικειμένων ενός συγκεκριμένου τύπου αυξάνεται χωρίς μείωση. Η διαδικασία διάγνωσης περιλαμβάνει τρία στάδια.

Πρώτο στάδιο — οπτικός έλεγχος μέσω Memory Profiler στο Android Studio. Ανοίξτε την καρτέλα Memory, εκτελέστε την ενέργεια-στόχο (ανοίξτε και κλείστε την οθόνη), πατήστε GC (Garbage Collection) και δείτε αν η μνήμη επέστρεψε στο αρχικό επίπεδο. Αν μετά από 3–4 κύκλους ανοίγματος-κλεισίματος η μνήμη αυξάνεται σταθερά — υπάρχει διαρροή.

Δεύτερο στάδιο — λήψη Heap Dump. Στο Memory Profiler πατήστε Dump Java Heap. Το αρχείο .hprof που προκύπτει ανοίξτε το στο Android Studio: θα δείτε όλα τα αντικείμενα στο heap με μεγέθη και αναφορές. Αναζητήστε κλάσεις των οποίων ο αριθμός θα πρέπει να είναι μηδέν μετά το κλείσιμο της οθόνης. Για παράδειγμα, MainActivity με αριθμό 2 μετά το κλείσιμο — προφανής διαρροή.

Τρίτο στάδιο — ανάλυση Retained Size και GC Root. Στο Android Studio αναλύστε το Retained Size: πόση μνήμη θα ελευθερωθεί αν διαγράψετε αυτό το αντικείμενο. Η διαδρομή από GC Root προς το αντικείμενο δείχνει τι το κρατά: Static field → HashMap → Activity — και βλέπετε το σημείο διαρροής. Το πλαίσιο Reference widget δείχνει όλους τους κατόχους του αντικειμένου.

Εργαλεία για εύρεση διαρροών

Τέσσερα εργαλεία καλύπτουν την εύρεση διαρροών από την αυτόματη ανίχνευση έως τη βαθιά ανάλυση Heap Dump.

ΕργαλείοΜέθοδοςΜορφή αποτελεσμάτων
LeakCanaryΑυτόματη παρακολούθησηHeap Dump + stack trace διαρροής
Android Memory ProfilerΧειροκίνητη παρακολούθησηΓράφημα μνήμης + Heap Dump
MAT (Eclipse)Βαθιά ανάλυσηΑναφορά Dominator Tree + διαδρομή GC Root
PerfettoSystem-wide ιχνηλάτησηΧρονοδιάγραμμα + native μνήμη

LeakCanary — must-have για κάθε έργο Android. Ανιχνεύει αυτόματα διαρροές μετά το τέλος του κύκλου ζωής Activity/Fragment και δείχνει την ακριβή θέση της διαρροής με stack trace. Ενσωμάτωση: μία γραμμή στο build.gradle. Το LeakCanary 2.x δεν απαιτεί χειροκίνητη αρχικοποίηση — καταχωρεί αυτόματα το Application Watcher.

Πώς να αποτρέψετε διαρροές μνήμης

Πρόληψη διαρροών ενσωματώνεται στη διαδικασία ανάπτυξης μέσω ενός συνόλου κανόνων και εργαλείων που ελέγχουν τον κώδικα σε κάθε στάδιο.

Κανόνας ισχυρών αναφορών

Ποτέ μην αποθηκεύετε αναφορά σε Activity, Fragment ή View σε στατικό πεδίο, singleton ή μακρόβιο αντικείμενο. Αν η αναφορά είναι αναπόφευκτη — χρησιμοποιήστε WeakReference ή αποθηκεύστε δεδομένα μέσω ViewModel που ζει ακριβώς όσο χρειάζεται και δεν κρατά απευθείας το View.

Lifecycle-aware αρχιτεκτονική

ViewModel και LiveData από Android Architecture Components λύνουν το πρόβλημα του κύκλου ζωής σε επίπεδο αρχιτεκτονικής. Το ViewModel επιβιώνει από περιστροφή οθόνης και δεν περιέχει αναφορές σε View. Το LiveData κάνει αυτόματα απεγγραφή του observer σε onDestroy(). Χρησιμοποιήστε τα αντί για χειροκίνητη εγγραφή σε υπηρεσίες συστήματος.

Code Review με εστίαση σε GC Root

Στο code review δώστε προσοχή σε: στατικά πεδία με τύπους Context/View, ανώνυμες κλάσεις, lambda που κλείνουν Activity, χειροκίνητες συνδρομές, RxJava disposable χωρίς composite, αποθήκευση Fragment μέσω Bundle. Σε Kotlin επιπλέον ελέγξτε τα coroutines για launch χωρίς σύνδεση με τον κύκλο ζωής.

Αυτόματος έλεγχος σε CI

LeakCanary μπορεί να λειτουργεί ως μέρος του pipeline δοκιμών: εκτελέστε δοκιμές αποδοχής με LeakCanary και σημειώστε το build ως αποτυχημένο αν βρεθεί διαρροή. Αυτό αποτρέπει την είσοδο διαρροών στην παραγωγή. Συμπληρώστε τον έλεγχο με Android Lint με τον κανόνα StaticFieldLeak — βρίσκει πιθανές διαρροές σε επίπεδο στατικής ανάλυσης.

kotlin
// LeakCanary σε δοκιμές
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // fail αν υπάρχει διαρροή
    }
}

Συχνές Ερωτήσεις

Πώς διαφέρει η διαρροή μνήμης από το OutOfMemoryError;

Η διαρροή είναι η αιτία, και το OutOfMemoryError είναι η συνέπεια. Μία διαρροή δεν οδηγεί σε OOM, αλλά η συσσώρευση δεκάδων διαρροών εξαντλεί το Heap. Το OOM είναι θανατηφόρα εξαίρεση, και η διαρροή είναι ένα μοτίβο που οδηγεί σε αυτήν με την πάροδο του χρόνου.

Πώς να βρείτε διαρροή χωρίς LeakCanary;

Μέσω Android Memory Profiler: ανοίξτε και κλείστε την οθόνη 5 φορές, μετά από κάθε κλείσιμο καλέστε GC. Αν η μνήμη δεν επιστρέφει στο βασικό επίπεδο — υπάρχει διαρροή. Πάρτε Heap Dump και βρείτε στη λίστα την κλάση Activity της οποίας ο αριθμός είναι μεγαλύτερος από 0 μετά το κλείσιμο.

Μπορεί η Kotlin να αποτρέψει διαρροές σε επίπεδο γλώσσας;

Μερικώς. Η Kotlin λύνει το πρόβλημα null-safety, αλλά δεν διαχειρίζεται strong references. Τα coroutines με lifecycleScope και viewModelScope αποτρέπουν διαρροές από εργασίες παρασκηνίου, και τα sealed class και data class μειώνουν τον αριθμό καταστάσεων που οδηγούν σε διαρροές. Η κύρια προστασία — αρχιτεκτονικά μοτίβα, όχι λειτουργίες γλώσσας.

Γιατί το LeakCanary βρίσκει διαρροή που δεν υπάρχει;

LeakCanary μερικές φορές δίνει false positive: ένα αντικείμενο μπορεί προσωρινά να κρατιέται από το σύστημα (π.χ. το InputMethodManager κρατά το τελευταίο View). Ελέγξτε χειροκίνητα: αν Retained Size < 1 KB και το GC Root είναι υπηρεσία συστήματος, πιθανότατα είναι ψευδής συναγερμός.

Οι διαρροές μνήμης συμβαίνουν μόνο σε Android;

Όχι. Διαρροές είναι δυνατές σε οποιαδήποτε πλατφόρμα με GC: iOS (Swift/Objective-C), Flutter (Dart), προγράμματα περιήγησης (JavaScript). Οι μηχανισμοί είναι οι ίδιοι — strong reference από GC Root. Σε iOS, η ARC διαχειρίζεται αυτόματα τη μνήμη, αλλά ο retain cycle μεταξύ αντικειμένων δημιουργεί την ίδια διαρροή.

Σύνοψη

  • Memory Leak — αντικείμενο που ο GC δεν μπορεί να ελευθερώσει λόγω ξεχασμένης strong reference
  • Στατικές αναφορές σε Activity και Context — η πιο συχνή αιτία διαρροών
  • Σιωπηρές αναφορές μέσω ανώνυμων κλάσεων, lambda και συνδρομών RxJava είναι πιο ύπουλες από τις ρητές
  • LeakCanary βρίσκει αυτόματα διαρροές και δείχνει ακριβές stack trace
  • Lifecycle-aware components (ViewModel, LiveData, lifecycleScope) εξαλείφουν μια κατηγορία διαρροών
  • Heap Dump και ανάλυση Retained Size — η κύρια μέθοδος χειροκίνητης διάγνωσης
  • Πρόληψη περιλαμβάνει code review σε strong reference και έλεγχο CI με LeakCanary

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

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

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

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