Android uygulamalarında Fragment, Jetpack Compose ve callback kaynaklı bellek sızıntılarını LeakCanary ve Android Studio Heap Profiler ile yeniden üretin, ölçün ve regresyona karşı koruyun.
Android Eğitimi: LeakCanary ile Bellek Sızıntılarını İzlemek
Android eğitimi için sızıntıyı yeniden üretilebilir hale getirin
Birkaç kez ekran açıp kapatınca RAM grafiğinin yükselmesi tek başına kanıt değildir: ART'nin heap'i büyütüp hemen işletim sistemine geri vermemesi normaldir. Kanıt olarak, ekran kapandıktan sonra ilgili Fragment veya View nesnesinin dominator tree içinde hâlâ güçlü referansla tutulduğunu arayın. Debug varyantına LeakCanary ekleyin; bu bağımlılık release APK'sına girmemelidir.
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android")
}LeakCanary, Activity/Fragment yok edildikten sonra weak reference'ın temizlenmesini bekler; nesne hâlâ erişilebiliyorsa heap dump alır ve GC root zincirini gösterir. Bu nedenle yalnızca bellek kullanım eğrisinden daha güvenilir bir başlangıç noktasıdır.Aşağıdaki hata özellikle uzun ömürlü singleton event bus'larda görülür: bağlı method reference, ProfileFragment örneğini güçlü biçimde tutar. Ekrandan geri çıkıldığında registry süreç ömründe yaşadığı için Fragment toplanamaz.
object ConnectivityRegistry {
private val listeners = mutableSetOf<(Boolean) -> Unit>()
fun add(listener: (Boolean) -> Unit) { listeners += listener }
fun remove(listener: (Boolean) -> Unit) { listeners -= listener }
}
class ProfileFragment : Fragment() {
private val listener: (Boolean) -> Unit = ::renderConnectivity
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ConnectivityRegistry.add(listener)
}
// Hatalı sürümde bu satır yoktu.
override fun onDestroyView() {
ConnectivityRegistry.remove(listener)
super.onDestroyView()
}
private fun renderConnectivity(online: Boolean) = Unit
}İncelik: ConnectivityRegistry.remove(::renderConnectivity) yazmak yerine listener'ı alan olarak saklayın. Her method-reference ifadesinin eşitlik davranışına güvenmek, özellikle wrapper/lambda katmanlarında yanlış listener'ı silmeye yol açabilir.Bir android kursu veya mobil uygulama geliştirme eğitimi laboratuvarında bu senaryoyu otomatikleştirmek için aynı Fragment'ı 10 kez açıp geri çıkın, son navigasyondan sonra LeakCanary'nin bildirimi ve referans yolunu kaydedin. Android eğitim müfredatında yalnızca "GC çalışır" demek yerine, zinciri ConnectivityRegistry → listeners → ProfileFragment olarak okuyabilmek hedeflenmelidir. Bu zincirdeki ilk süreç-ömürlü nesne, düzeltmenin hangi katmanda yapılacağını doğrudan söyler.
Android Studio eğitimi: Heap Profiler ile önce-sonra karşılaştırması
Android Studio Heap Profiler'da ölçümü aynı koşulda yapın: uygulamayı temiz başlatın, hedef ekranı 10 kez açıp kapatın, son ekran kapandıktan sonra GC düğmesine basın ve heap dump alın. Dump içinde ProfileFragment sınıfını aratın; Instance Count, Shallow Size ve özellikle Retained Size değerlerini not edin. Shallow size yalnız nesnenin kendi alanlarını, retained size ise o nesne bırakılırsa toplanabilecek alt grafiği gösterir; bir Fragment'ın küçük shallow size'ı büyük bir View hiyerarşisini sakladığını gizleyebilir.
CLI ile CI dışı tekrar doğrulaması için debug edilebilir süreçte dump alıp Android Studio'da açabilirsiniz.
PACKAGE=com.example.app
PID=$(adb shell pidof -s "$PACKAGE")
adb shell am dumpheap "$PID" /data/local/tmp/app-before.hprof
adb pull /data/local/tmp/app-before.hprof ./app-before.hprof
adb shell dumpsys meminfo "$PACKAGE"Listener kaldırma değişikliğinden sonra aynı kullanıcı akışıyla app-after.hprof üretin. Başarı kriteri, yalnızca meminfo içindeki toplam PSS'nin düşmesi değildir; GC sonrasında hedef Fragment sayısının sıfıra inmesi ve dominator tree'de registry altında artık View/Fragment kalmamasıdır. PSS; bitmap havuzu, JIT ve allocator sayfaları nedeniyle iki kısa koşu arasında oynayabilir.Profiling sırasında yaygın hata, dump'ı ekran hâlâ back stack'teyken almaktır. Navigation back stack'te duran Fragment'ın tutulması beklenen davranıştır. Testte hedef destination'dan gerçekten popBackStack() ile çıkın veya Activity'yi kapatın; ardından ana thread'in cleanup işlerini tamamlaması için birkaç frame bekleyin. Bu kontrol, gerçek kaçak ile kasıtlı UI saklamayı ayırır.
Android MVVM mimarisi içinde callback ve coroutine sahipliği
android mvvm mimarisi, ViewModel kullanıldığı için otomatik olarak sızıntısız olmaz. Asıl kural şudur: repository ya da singleton, Activity/Fragment/View referansı tutamaz; UI'ya veri akışı Flow veya suspend API ile dönmelidir. Callback tabanlı bir istemciyi Flow'a çevirirken awaitClose zorunludur. İptal edilen collector callback'i silmezse, client ViewModel'i; ViewModel de yanlış tasarlanmışsa UI'yı dolaylı olarak tutabilir.
fun LocationClient.locations(): Flow<Location> = callbackFlow {
val callback = object : LocationCallback() {
override fun onLocationChanged(value: Location) {
trySend(value).isSuccess
}
}
register(callback)
awaitClose { unregister(callback) }
}trySend sonucu başarısız olduğunda callback'i burada yeniden kaydetmeyin: kanal kapanmış olabilir. Kayıt/iptal eşleşmesini tek bir awaitClose bloğunda tutmak, çift unregister yarışını da azaltır.ViewModel içinde uzun ömürlü SDK istemcisi oluşturmanız gerekiyorsa Fragment context'i değil applicationContext kullanın. Ancak bu yalnız Context sızıntısını önler; callback temizliğinin yerine geçmez.
class MapRepository(appContext: Context) {
private val client = VendorLocationClient(appContext.applicationContext)
fun stream() = client.locations()
}
class MapViewModel(repository: MapRepository) : ViewModel() {
val locations = repository.stream()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
}WhileSubscribed(5_000) kısa ekran geçişlerinde upstream'i beş saniye canlı tutabilir; bu bir kaçak değildir, ama profiler ölçümünü etkiler. Testten önce bu zaman aşımının bitmesini bekleyin veya test odaklı akışta değeri sıfırlayın. Kotlin eğitimi ve kotlin kursu içeriklerinde bu ayrımın atlanması, "scope iptal oldu, her şey temizlendi" yanılgısına neden olur.Jetpack Compose side effect'lerinde gözden kaçan sahiplik sınırı
Jetpack Compose, composable composition'dan çıktığında coroutine'i iptal edebilir; fakat sizin kaydettiğiniz Java/Kotlin callback'ini kendiliğinden silemez. Harita, medya oynatıcı veya analytics observer'ı gibi imperative API'lerde DisposableEffect ile kayıt ve kaldırmayı aynı blokta eşleyin.
@Composable
fun PlayerEvents(player: Player, onEnded: () -> Unit) {
val latestOnEnded by rememberUpdatedState(onEnded)
DisposableEffect(player) {
val listener = object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_ENDED) latestOnEnded()
}
}
player.addListener(listener)
onDispose { player.removeListener(listener) }
}
}rememberUpdatedState burada listener'ı her callback değişiminde söküp takmak yerine en güncel lambda'yı okutmaktadır. Buna karşılık player değiştiğinde DisposableEffect yeniden başlar; key'e player eklenmezse eski player dinleyiciyi tutmaya devam eder.Compose ekranı bir Fragment içinde barınıyorsa ikinci bir yaşam döngüsü sınırı vardır: Fragment yaşayabilirken view yok olabilir. ComposeView için composition stratejisini açıkça belirleyin.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
binding.composeView.setContent { PlayerEvents(player, viewModel::onEnded) }
}Bu strateji, view lifecycle sona erdiğinde composition'ın dispose edilmesini sağlar. Özellikle ViewPager2 veya yeniden kullanılan Fragment view'larında varsayılan stratejiye dair varsayım yapmak yerine LeakCanary ile kapanış anını doğrulayın.Regresyonu test ve play store yayınlama sürecine bağlamak
Sızıntı düzeltmesini sadece manuel profiler oturumuna bırakmayın. En azından instrumentation test'te ekranı açıp kapatan yolun listener remove çağrısını doğrulayın; registry'yi test edilebilir bir arayüz arkasına almak bunu kolaylaştırır.
@Test
fun leavingProfile_removesConnectivityListener() {
val registry = FakeConnectivityRegistry()
launchProfile(registry).use { scenario ->
scenario.moveToState(Lifecycle.State.DESTROYED)
}
assertThat(registry.listenerCount).isEqualTo(0)
}Bu test heap'te nesnenin kesin toplanacağını garanti etmez, ancak sızıntı zincirinin kökündeki deterministik abonelik hatasını yakalar. Heap tabanlı assertion'lar GC zamanlamasına bağlı olduğundan birim testte flaky olmaya daha yatkındır.CI'da ./gradlew connectedCheck sonrasında debug build'deki LeakCanary raporlarını artifact olarak saklayın ve yeni bir leak signature çıktığında inceleme zorunluluğu koyun. play store yayınlama için oluşturulan release varyantında LeakCanary'yi eklemeyin; debugImplementation bunun içindir. Release öncesi fiziksel cihazda aynı navigasyon senaryosundan iki heap dump almak, ProGuard/R8 sonrası callback veya reflection davranışı değişen üçüncü taraf SDK'ları ayrıca görünür kılar.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
Android Studio eğitimi sırasında Heap Profiler'da retained size nasıl yorumlanır?
Önce GC çalıştırın, sonra heap dump alın ve hedef sınıfın dominator tree konumuna bakın. Retained size, nesne serbest bırakılırsa toplanabilecek grafiği ifade eder; Fragment'ın retained size'ı büyükse GC Roots yolunda singleton, static field, thread veya listener koleksiyonu arayın. Sadece allocation sayısına bakmak yeterli değildir; çok allocation yapıp hiç nesne tutmayan ekran kaçak üretmeyebilir.
Kotlin eğitimi için callbackFlow awaitClose neden gereklidir?
Collector iptal edildiğinde callback tabanlı API'lerin kaydını coroutine runtime otomatik kaldırmaz. awaitClose { unregister(callback) }, Flow kanalı kapandığında unregister işlemini çalıştırır. Callback nesnesini local değişkende üretip aynı örneği unregister etmek gerekir; yeni bir callback örneği oluşturarak kaldırmaya çalışmak çoğu SDK'da etkisizdir.
Jetpack Compose ekranında DisposableEffect mi LaunchedEffect mi kullanmalıyım?
Suspend iş başlatmak için LaunchedEffect, imperative listener/observer kaydı için DisposableEffect kullanın. Örneğin player.addListener() çağrısının karşılığı onDispose { player.removeListener(listener) } olmalıdır. Key listesine dinlenen kaynak nesneyi, örneğin player'ı ekleyin; yalnız callback'i key yapmak gereksiz sök-tak üretir.
Android kursu projesinde LeakCanary release build'e eklenmeli mi?
Hayır. Bağımlılığı debugImplementation ile sınırlayın. Böylece analizci yalnız debug/test varyantında çalışır; release artifact'ına izleme bileşenleri eklenmez. Release adayında ise Android Studio Heap Profiler veya kontrollü am dumpheap oturumuyla aynı kullanıcı akışını tekrar ölçün.
AI / LLM Discovery
Bu makale Opendart Akademi Android eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.


