Memory Leak Nedir ve Neden Olur?
Geri Vermediğiniz Belleğin Bedeli
Bir uygulamayı uzun süre açık bıraktınız — başlangıçta pürüzsüz çalışıyorken zamanla yavaşlamaya, takılmaya, hatta çökmeye başlıyor. Bellek kullanımı sürekli artıyor ama düşmüyor. Büyük ihtimalle karşı karşıya olduğunuz şey bir memory leak'tir.
Memory leak, yazılım dünyasının sessiz ama yıkıcı sorunlarından biridir. Hemen fark edilmez, test sırasında ortaya çıkmayabilir, ama production'da kullanıcı deneyimini ciddi biçimde bozar.
Memory Leak Nedir?
Memory leak, bir nesnenin artık ihtiyaç duyulmadığı halde bellekten serbest bırakılamamasıdır.
Android'de bellek yönetimi Garbage Collector (GC) tarafından yapılır. GC, artık kullanılmayan nesneleri tespit ederek bellekten temizler. Bir nesnenin "kullanılmıyor" sayılabilmesi için o nesneye ulaşan hiçbir referansın kalmaması gerekir.
İşte memory leak tam da bu noktada ortaya çıkar: Bir nesne artık işlevsel olarak gerekli değildir, ama ona hâlâ aktif bir referans tutulmaktadır. GC bu nesneye ulaşan bir referans gördüğü için onu temizleyemez. Nesne bellekte sonsuza kadar yaşamaya devam eder.
Android'e Özgü Tehlike: Context
Android'de memory leak'lerin büyük çoğunluğu Context nesnelerinin yanlış yönetiminden kaynaklanır.
Context, Android bileşenlerinin sistem kaynaklarına erişmesini sağlayan önemli bir nesnedir. Activity, Service, Application — bunların hepsi birer Context'tir. Ve Activity Context, o Activity'nin tüm View hiyerarşisini, layout'ını ve kaynaklarını bünyesinde barındırır. Yani bellek açısından son derece ağır bir nesnedir.
Bir Activity'nin Context'ini statik bir değişkende sakladığınızı düşünün. Activity ekrandan kaldırıldığında, yönlendirildiğinde ya da döndürüldüğünde Android bu Activity'yi bellekten temizlemek ister. Ama statik değişken hâlâ o Activity'nin referansını tutuyor. GC bu nesneyi temizleyemez. Activity bellekte sıkışıp kalır — ve onunla birlikte tüm View hiyerarşisi.
En Yaygın Memory Leak Senaryoları
Statik referanslar en sık karşılaşılan sebeplerden biridir. Statik bir değişkene Activity ya da View referansı atamak, o nesnenin uygulama yaşam süresi boyunca bellekte kalmasına neden olur. Statik bağlamda yalnızca Application Context kullanılmalıdır.
Anonim inner class'lar ve lambda'lar sinsi bir leak kaynağıdır. Bir Activity içinde tanımlanan anonim sınıf, dış Activity'ye örtük bir referans taşır. Bu anonim sınıf bir callback olarak uzun ömürlü bir nesneye verilirse Activity'nin referansı da onunla birlikte hayatta kalır.
Handler ve Runnable kombinasyonu yaygın bir tuzaktır. Bir Handler'a gecikmiş bir Runnable gönderdiniz — 10 saniye sonra çalışacak. Ama kullanıcı o süre dolmadan uygulamadan çıktı. Handler hâlâ Runnable'ı bekliyor, Runnable Activity'ye referans tutuyor, Activity bellekten silinemiyor.
Listener'ların kaldırılmaması özellikle sistem servislerine kayıtlı listener'larda sık görülür. LocationManager, SensorManager, BroadcastReceiver gibi servislere kayıt yaptırdıysanız, Activity'nin yaşam döngüsü sona erdiğinde bu kayıtları kaldırmanız zorunludur. Kaldırmazsanız sistem o Activity'ye referans tutmaya devam eder.
ViewModel içinde Activity referansı yanlış kullanımın özellikle ironik bir örneğidir. ViewModel, Activity'nin yeniden oluşturulmasında hayatta kalmak için tasarlanmıştır. Bir ViewModel'e Activity Context geçirirseniz, Activity yeniden oluştuğunda eski Activity ViewModel üzerinden bellekte yaşamaya devam eder.
Memory Leak Nasıl Fark Edilir?
Memory leak'in en belirgin belirtisi uygulamanın zamanla yavaşlaması ve bellek kullanımının sürekli artmasıdır. Ekran döndürme ya da farklı ekranlar arasında gidip gelmek leak'i hızlandırır çünkü her seferinde yeni nesneler oluşturulur ama eskiler temizlenemez.
Android Studio'nun Memory Profiler aracı bu sorunu tespit etmek için vazgeçilmezdir. Heap dump alarak bellekte hangi nesnelerin ne kadar yer kapladığını ve bu nesnelerin neden temizlenemediğini görselleştirir.
LeakCanary ise Android geliştirme dünyasının bu alandaki en popüler kütüphanesidir. Development aşamasında otomatik olarak Activity ve Fragment leak'lerini tespit eder, leak zincirini ayrıntılı biçimde raporlar ve sizi doğrudan sorunun kaynağına yönlendirir.
Geliştirici Perspektifinden Bakış
Memory leak'e karşı savunmanın en iyi yolu önleyici alışkanlıklar geliştirmektir.
Activity ya da View referansı tutmanız gereken bir durumda kendinize şunu sorun: Bu referansı tutan nesne Activity'den daha uzun yaşayabilir mi? Cevap evetse ya WeakReference kullanın ya da mimarinizi yeniden gözden geçirin.
Sistem servislerine kayıt yaptırdığınız her noktanın onDestroy ya da onStop içinde bir karşılığı olduğundan emin olun. Her register'ın bir unregister'ı, her addListener'ın bir removeListener'ı olmalıdır.
Ve her yeni özelliği geliştirirken LeakCanary'yi açık tutun. Leak'leri production'da bulmak, geliştirme aşamasında bulmaktan çok daha maliyetlidir.