• 2.09.2026 21:07:14
  • Admin Admin

Jetpack Compose ekranlarında ana thread'i bekleten coroutine işlerini Perfetto ile teşhis edin. Bu android eğitimi, dispatcher seçimi, Flow operatörleri ve Macrobenchmark ile ölçülebilir bir düzeltme akışı sunar.

Android Eğitimi: Coroutine Dispatcher Tıkanmasını Perfetto ile Çözmek

Android eğitimi: Sorunu tekrar üretilebilir bir iz haline getirin

Compose listesinde kaydırma sırasında takılma görüyorsanız ilk şüpheli her zaman recomposition değildir. Repository katmanındaki JSON ayrıştırma, liste sıralama, görsel metadata çıkarma veya büyük Room entity listesini UI modele dönüştürme işi ana thread'de çalışıyorsa, Choreographer frame callback'i zamanında başlayamaz. Sorunu kayda almak için test cihazında aynı veri hacmiyle 15 saniyelik bir Perfetto izi alın. Release'e yakın bir build kullanın; debug build'in ek doğrulama maliyeti scheduler davranışını değiştirebilir.

# Uygulamada problemli Feed ekranini acin ve kaydirmayi baslatin.
adb shell perfetto -o /data/local/tmp/feed-jank.pftrace -t 15s sched freq idle am wm gfx view
adb pull /data/local/tmp/feed-jank.pftrace ./feed-jank.pftrace

İzi Perfetto arayüzünde açıp uygulama prosesinin Main thread track'ini, RenderThread'i ve worker thread'lerini yan yana inceleyin. Bir frame çevresinde Main thread'de uzun bir Kotlin fonksiyonu, ya da Main thread scheduler durumunda Runnable bekleme görüyorsanız, sorun ölçülebilir hale gelmiştir. İkinci durumda ana thread boşta değildir: CPU çekirdekleri Dispatchers.Default üzerindeki uzun işlerle dolu olabilir. Bu ayrım önemlidir; ilk durumda kodu main'den taşırsınız, ikinci durumda paralellik sınırını değiştirirsiniz.

Trace'de repository dönüşümünü framework dilimlerinden ayırmak için senkron CPU bloklarını adlandırın. Suspend fonksiyonun tamamını Trace.beginSection ile sarmalamayın; coroutine askıya alındığında begin ve end farklı thread'lerde kalabilir. Sadece kesintisiz çalışan dönüşüm bloğunu etiketleyin.

private fun mapRows(rows: List<ArticleRow>): List<ArticleCard> {
    Trace.beginSection("feed:mapRows")
    return try {
        rows.asSequence()
            .filter { it.published }
            .sortedByDescending { it.publishedAt }
            .map { ArticleCard(id = it.id, title = it.title, date = it.publishedAt) }
            .toList()
    } finally {
        Trace.endSection()
    }
}

Perfetto'da dispatcher doygunluğunu doğru okuyun

Perfetto'da yalnızca uzun Main thread slice'ına bakmak yanlış kök neden üretebilir. Main thread'in scheduler track'inde bir dilim uzun süre Runnable durumunda bekliyor, buna karşılık DefaultDispatcher-worker thread'leri CPU'da sürekli Running görünüyorsa CPU rekabeti vardır. Main thread slice'ı doğrudan feed:mapRows altında uzuyorsa iş yanlış dispatcher'dadır. Bu iki senaryo aynı kullanıcı belirtisini verir, fakat ilki context değişimi, ikincisi iş kuyruğu kapasitesi ve algoritma düzeltmesi ister.

İnceleme sırasında feed:mapRows slice'ının süresini ve onu izleyen doFrame başlangıcına kadar geçen süreyi not edin. Sabit 16 ms eşiğini evrensel kural saymayın; cihazın yenileme hızına göre frame bütçesi değişir. Karşılaştırılabilir sonuç için önce ve sonra aynı cihazda, aynı ekran kaydırma senaryosunda en az 10 trace alın. Medyan ve en kötü 10 percentil değerlerini kaydedin; tek bir iyi trace, scheduler gürültüsünü gizleyebilir.

Android Studio eğitimi içinde sık görülen hata, CPU işi için Dispatchers.IO kullanmaktır. IO dispatcher bloklayan ağ veya dosya çağrılarını telafi etmek için tasarlanmıştır ve yüksek paralelliğe izin verebilir. JSON parse, sıralama ve diff üretimi CPU işidir. Bunları IO'ya göndermek, daha fazla worker'ın çekirdekleri tüketmesine ve Main thread'in Runnable beklemesinin uzamasına yol açabilir.

Kotlin eğitimi: Flow zincirinde CPU işini sınırlayın

Aşağıdaki düzenleme Room'dan gelen satırları UI modeline dönüştüren CPU işini paylaşılan, sınırlı bir dispatcher'a taşır. flowOn yalnızca kendisinden upstream'deki operatörlerin context'ini değiştirir. Bu nedenle flowOn satırını map'ten sonra koymak kritiktir; map'ten önce koyarsanız map collector context'inde, çoğu ViewModel'de Main üzerinde kalabilir.

class FeedRepository(
    private val dao: ArticleDao,
    private val cpuDispatcher: CoroutineDispatcher = Dispatchers.Default.limitedParallelism(2)
) {
    fun observeFeed(): Flow<FeedUi> = dao.observePublished()
        .map { rows -> FeedUi.Content(mapRows(rows)) }
        .onStart { emit(FeedUi.Loading) }
        .catch { error -> emit(FeedUi.Error(error)) }
        .flowOn(cpuDispatcher)
}

class FeedViewModel(repository: FeedRepository) : ViewModel() {
    val uiState = repository.observeFeed()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), FeedUi.Loading)
}

limitedParallelism(2) için doğru sayı benchmark ile bulunur; 2 evrensel bir değer değildir. Dört büyük dönüşüm aynı anda başlıyorsa sınırsız Default kullanımı küçük çekirdekli cihazlarda UI thread'i aç bırakabilir. Buna karşılık paralelliği 1 yapmak da yeni veri geldiğinde kuyruk gecikmesini büyütebilir. Perfetto'da Default worker'ların aynı anda CPU'da kaldığı süreyi ve Macrobenchmark'taki frame overrun dağılımını ölçerek 1, 2 ve 3 değerlerini deneyin.

Kotlin kursu örneklerinde yaygın bir tuzak da withContext kullanmadan önce CPU işini hesaplamaktır. Aşağıdaki ilk sürümde normalizeTitles Main üzerinde yürür, çünkü argüman değerlendirmesi withContext'e girmeden gerçekleşir. İkinci sürümde dönüşüm dispatcher bloğunun içindedir.

// Yanlis: normalizeTitles Main thread'de calisabilir.
val result = withContext(Dispatchers.Default) {
    repository.save(normalizeTitles(input))
}

// Dogru: CPU donusumu dispatcher degistikten sonra calisir.
val result = withContext(Dispatchers.Default.limitedParallelism(2)) {
    repository.save(normalizeTitles(input))
}

Jetpack Compose'da gereksiz UI emisyonlarını azaltın

Dispatcher düzeltmesi tek başına yeterli olmayabilir. Backend'den gelen her polling sonucu aynı görünür alanları taşıyorsa, Compose state'e her listeyi yazmak snapshot apply ve layout işini tetikler. Repository veya ViewModel sınırında UI'nın gerçekten çizdiği alanlara göre distinctUntilChanged uygulayın. Tüm entity listesinin equals maliyeti de büyük olabilir; bu nedenle yalnızca görünür kimlikler ve güncellenme işaretini taşıyan küçük, immutable bir render key kullanın.

data class FeedRenderKey(
    val ids: List<Long>,
    val newestUpdatedAt: Long
)

val uiState: StateFlow<FeedUi> = repository.observeFeed()
    .distinctUntilChangedBy { state ->
        when (state) {
            is FeedUi.Content -> FeedRenderKey(
                ids = state.items.map { it.id },
                newestUpdatedAt = state.items.maxOfOrNull { it.updatedAt } ?: 0L
            )
            else -> state::class
        }
    }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), FeedUi.Loading)

Burada edge case, ekranda başlık veya okunma durumu değişebildiği halde render key'e yalnızca id eklemektir. Bu durumda StateFlow yeni değeri bastırır ve UI eski başlığı gösterir. Key, composable'ın okuduğu her alanı temsil etmelidir. Bu nedenle android mvvm mimarisi içinde entity'yi doğrudan UI'ya sızdırmak yerine, ekranın render kontratını ayrı bir immutable modelle tanımlamak hata ayıklamayı kolaylaştırır.

Composable tarafta lifecycle'a bağlı toplama için androidx.lifecycle:lifecycle-runtime-compose içindeki collectAsStateWithLifecycle kullanın. Bu, ekran STOPPED durumundayken collector'ı durdurur; ancak upstream'in gerçekten durması için stateIn tarafındaki SharingStarted.WhileSubscribed yapılandırmasının da bulunması gerekir. Eagerly kullanılırsa ekran kapalıyken CPU dönüşümü devam eder ve Perfetto'da görünmeyen arka plan rekabeti oluşturabilir.

Mobil uygulama geliştirme eğitimi için önce-sonra Macrobenchmark kapısı

Düzeltmeyi 'kaydırma daha akıcı hissettiriyor' diye kabul etmeyin. Macrobenchmark modülünde aynı veri setini açan test yazın, FrameTimingMetric ile frame overrun dağılımını ve TraceSectionMetric ile feed:mapRows süresini birlikte kaydedin. Benchmark uygulamasının manifest'ine profileable tanımı eklemek, release benzeri pakette shell profiler erişimini sağlar.

<application ...>
    <profileable android:shell="true" />
</application>

@RunWith(AndroidJUnit4::class)
class FeedScrollBenchmark {
    @get:Rule val benchmarkRule = MacrobenchmarkRule()

    @Test fun scrollFeed() = benchmarkRule.measureRepeated(
        packageName = "com.example.app",
        metrics = listOf(
            FrameTimingMetric(),
            TraceSectionMetric("feed:mapRows")
        ),
        compilationMode = CompilationMode.None(),
        startupMode = StartupMode.WARM,
        iterations = 10,
        setupBlock = {
            pressHome()
            startActivityAndWait()
        }
    ) {
        device.findObject(By.res("feed_list"))
            .fling(Direction.DOWN)
    }
}

Önce değişiklik yapmadan JSON çıktısındaki frameOverrunMs percentile değerlerini ve feed:mapRows metriklerini CI artefact'ı olarak saklayın. Sonra limitedParallelism, Flow yerleşimi ve render key değişikliklerini uygulayıp aynı fiziksel cihazda tekrar çalıştırın. Sadece ortalamayı değil, negatif olmayan overrun örneklerinin sayısını da karşılaştırın; ortalama iyileşirken birkaç çok kötü frame kullanıcı tarafından fark edilmeye devam edebilir. Bu ölçüm disiplini, android kursu veya mobil uygulama geliştirme eğitimi laboratuvarlarında cihazlar arası sonuçları da denetlenebilir yapar.

Bu optimizasyon play store yayınlama öncesinde özellikle önemlidir: benchmark'ı yalnızca geliştirici makinesindeki amiral gemisi cihazda değil, desteklenen düşük CPU kapasiteli bir fiziksel cihazda çalıştırın. Emulator scheduler'ı host yükünden etkilendiği için dispatcher doygunluğu ölçümünde güvenilir bir son karar mekanizması değildir.

Sık Sorulan Sorular

Android eğitiminde Perfetto ile Main thread Runnable beklemesi nasıl yorumlanır?

Main thread scheduler durumunda Runnable görünürken DefaultDispatcher-worker thread'leri CPU'da Running ise ana thread kendi kodunda bloklu değildir, CPU için sıra bekliyordur. Perfetto'da aynı zaman aralığındaki worker slice'larını inceleyin; ardından CPU dönüşümlerini Dispatchers.Default.limitedParallelism(1), 2 ve 3 ile ölçüp Macrobenchmark frameOverrunMs sonuçlarını karşılaştırın.

Kotlin eğitimi için flowOn map operatöründen önce mi sonra mı yazılmalı?

map işlemini arka plana taşımak için flowOn map'ten sonra yazılmalıdır. Flow'da flowOn upstream tarafı etkiler. dao.observe().flowOn(cpu).map { agirDonusum(it) } ifadesindeki map collector context'inde kalabilir; dao.observe().map { agirDonusum(it) }.flowOn(cpu) ise map'i cpu dispatcher üzerinde çalıştırır.

Jetpack Compose uygulamasında Dispatchers.IO kullanmak kaydırma jank'ini çözer mi?

JSON parse, sıralama, diff üretimi gibi CPU işleri için doğrudan çözüm değildir. IO dispatcher yüksek paralellikte worker açabildiğinden çekirdekleri doldurup Main thread'in scheduler beklemesini büyütebilir. Perfetto'da worker doygunluğu görülüyorsa Dispatchers.Default.limitedParallelism ile sınır koyun ve FrameTimingMetric ile önce-sonra ölçün.

Android Studio eğitimi kapsamında Macrobenchmark sonucu CI'da nasıl karşılaştırılır?

Benchmark JSON çıktısını her ana branch çalışmasında artefact olarak saklayın. Aynı cihaz modeli, aynı test verisi, aynı compilationMode ve en az 10 iterasyon kullanın. frameOverrunMs percentilleri ile TraceSectionMetric sonucu için kabul eşiği tanımlayın; örneğin kötüleşme olduğunda yalnızca ortalama yerine p90 overrun ve mapRows toplam süresini birlikte raporlayı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.

Opendart Akademi llms.txt