Διαρροή μνήμης (Memory Leak) — κατάσταση όπου η εφαρμογή διατηρεί αναφορές σε αντικείμενα που δεν χρειάζονται πλέον, εμποδίζοντας τον garbage collector να απελευθερώσει την occupied memory. Σύμφωνα με τα δεδομένα του LeakCanary, ακόμη και σε καλογραμμένες εφαρμογές εμφανίζονται 3–5 διαρροές ανά 10.000 γραμμές κώδικα. Κάθε διαρροή σταδιακά μειώνει τη διαθέσιμη μνήμη, οδηγώντας σε επιβράδυνση και OutOfMemoryError.
Κύρια Σημεία
Διαρροή μνήμης (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% των περιπτώσεων στην κινητή ανάπτυξη. Κάθε ένας έχει τη δική του αιτία και χαρακτηριστικό μοτίβο στον κώδικα.
Η πιο γνωστή διαρροή σε Android — αποθήκευση στατικής αναφοράς σε Activity ή Context. Τυπικός κώδικας: ένα στατικό πεδίο Activity που δεν μηδενίζεται στο onDestroy(). Όσο το στατικό πεδίο ζει, ολόκληρο το Activity με το δέντρο View του, το οποίο μπορεί να καταλαμβάνει 1–10 MB, παραμένει ζωντανό. Αυτή είναι μια κλασική διαρροή που το LeakCanary βρίσκει πρώτα.
Λύση: ποτέ μην αποθηκεύετε Activity ή Context σε στατικά πεδία. Χρησιμοποιήστε Application Context για singleton που επιβιώνουν του Activity. Αν χρειάζεστε αναφορά σε Activity — χρησιμοποιήστε WeakReference<Activity>.
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() είναι δεμένο με τον κύκλο ζωής.
// αυτόματη απεγγραφή μέσω Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// coroutines με lifecycleScope
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
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 έχει δύο κύκλους ζωής: του ίδιου του 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 |
| Perfetto | System-wide ιχνηλάτηση | Χρονοδιάγραμμα + native μνήμη |
LeakCanary — must-have για κάθε έργο Android. Ανιχνεύει αυτόματα διαρροές μετά το τέλος του κύκλου ζωής Activity/Fragment και δείχνει την ακριβή θέση της διαρροής με stack trace. Ενσωμάτωση: μία γραμμή στο build.gradle. Το LeakCanary 2.x δεν απαιτεί χειροκίνητη αρχικοποίηση — καταχωρεί αυτόματα το Application Watcher.
Πρόληψη διαρροών ενσωματώνεται στη διαδικασία ανάπτυξης μέσω ενός συνόλου κανόνων και εργαλείων που ελέγχουν τον κώδικα σε κάθε στάδιο.
Ποτέ μην αποθηκεύετε αναφορά σε Activity, Fragment ή View σε στατικό πεδίο, singleton ή μακρόβιο αντικείμενο. Αν η αναφορά είναι αναπόφευκτη — χρησιμοποιήστε WeakReference ή αποθηκεύστε δεδομένα μέσω ViewModel που ζει ακριβώς όσο χρειάζεται και δεν κρατά απευθείας το View.
ViewModel και LiveData από Android Architecture Components λύνουν το πρόβλημα του κύκλου ζωής σε επίπεδο αρχιτεκτονικής. Το ViewModel επιβιώνει από περιστροφή οθόνης και δεν περιέχει αναφορές σε View. Το LiveData κάνει αυτόματα απεγγραφή του observer σε onDestroy(). Χρησιμοποιήστε τα αντί για χειροκίνητη εγγραφή σε υπηρεσίες συστήματος.
Στο code review δώστε προσοχή σε: στατικά πεδία με τύπους Context/View, ανώνυμες κλάσεις, lambda που κλείνουν Activity, χειροκίνητες συνδρομές, RxJava disposable χωρίς composite, αποθήκευση Fragment μέσω Bundle. Σε Kotlin επιπλέον ελέγξτε τα coroutines για launch χωρίς σύνδεση με τον κύκλο ζωής.
LeakCanary μπορεί να λειτουργεί ως μέρος του pipeline δοκιμών: εκτελέστε δοκιμές αποδοχής με LeakCanary και σημειώστε το build ως αποτυχημένο αν βρεθεί διαρροή. Αυτό αποτρέπει την είσοδο διαρροών στην παραγωγή. Συμπληρώστε τον έλεγχο με Android Lint με τον κανόνα StaticFieldLeak — βρίσκει πιθανές διαρροές σε επίπεδο στατικής ανάλυσης.
// LeakCanary σε δοκιμές
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // fail αν υπάρχει διαρροή
}
}
Συχνές Ερωτήσεις
Η διαρροή είναι η αιτία, και το OutOfMemoryError είναι η συνέπεια. Μία διαρροή δεν οδηγεί σε OOM, αλλά η συσσώρευση δεκάδων διαρροών εξαντλεί το Heap. Το OOM είναι θανατηφόρα εξαίρεση, και η διαρροή είναι ένα μοτίβο που οδηγεί σε αυτήν με την πάροδο του χρόνου.
Μέσω Android Memory Profiler: ανοίξτε και κλείστε την οθόνη 5 φορές, μετά από κάθε κλείσιμο καλέστε GC. Αν η μνήμη δεν επιστρέφει στο βασικό επίπεδο — υπάρχει διαρροή. Πάρτε Heap Dump και βρείτε στη λίστα την κλάση Activity της οποίας ο αριθμός είναι μεγαλύτερος από 0 μετά το κλείσιμο.
Μερικώς. Η Kotlin λύνει το πρόβλημα null-safety, αλλά δεν διαχειρίζεται strong references. Τα coroutines με lifecycleScope και viewModelScope αποτρέπουν διαρροές από εργασίες παρασκηνίου, και τα sealed class και data class μειώνουν τον αριθμό καταστάσεων που οδηγούν σε διαρροές. Η κύρια προστασία — αρχιτεκτονικά μοτίβα, όχι λειτουργίες γλώσσας.
LeakCanary μερικές φορές δίνει false positive: ένα αντικείμενο μπορεί προσωρινά να κρατιέται από το σύστημα (π.χ. το InputMethodManager κρατά το τελευταίο View). Ελέγξτε χειροκίνητα: αν Retained Size < 1 KB και το GC Root είναι υπηρεσία συστήματος, πιθανότατα είναι ψευδής συναγερμός.
Όχι. Διαρροές είναι δυνατές σε οποιαδήποτε πλατφόρμα με GC: iOS (Swift/Objective-C), Flutter (Dart), προγράμματα περιήγησης (JavaScript). Οι μηχανισμοί είναι οι ίδιοι — strong reference από GC Root. Σε iOS, η ARC διαχειρίζεται αυτόματα τη μνήμη, αλλά ο retain cycle μεταξύ αντικειμένων δημιουργεί την ίδια διαρροή.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης