Ders 08 / 19
Çizim Akıcılığı
Akıcı kaydırma, her karenin bütçesi içinde kalmasına bağlıdır. Ders kaydırılan bir listede her karenin bütün öğeleri yeniden oluşturmasını ölçer, kararlı anahtarın yeniden oluşturulan işi nasıl daralttığını ve kuralın aşımın büyüklüğünü neden görmediğini gösterir.
Yayın Güncelleme
Bu derste
- Kare bütçesini ve atlanan kareyi tanımlar
- Kaydırılan bir listede her karenin yeniden oluşturduğu öğe sayısını modelden ölçer
- Aynı kodun liste uzunluğuna göre içinde ya da hepsinde çıkabildiğini gösterir
- Kararlı anahtarın yeniden oluşturmayı ekrana giren öğelerle sınırladığını ve aşımı hepsinden azına indirdiğini ölçer
İçindekiler
Önceki derste ilk kare çizildi ve soğuk başlatmanın ağır işi başka bir bütçeye taşındı. Kullanıcı artık ekrana bakıyor ve bir listeyi kaydırıyor. Kaydırma sürdükçe her kare yeniden çizilir; her kare, ekranın yenileme hızına bağlı bir süre içinde hazır olmak zorundadır. Bu süreye kare bütçesi (frame budget) diyoruz. Bütçeye yetişemeyen kare atlanan kare (dropped frame) olur; kullanıcı bunu takılma olarak görür.
Bu derste süre ölçmüyoruz, çünkü bir ölçüm ortamının süresi güvenilir bir sayı değildir. Onun yerine kare başına yapılan işi sayıyoruz: bir karede kaç öğe yeniden oluşturuldu. Bütçe de aynı birimdedir. Ölçü kursun ölçüsüdür, yani bu bütçe kaç karede aşıldı.
Ölçü, kursun her dersinde aynı iki sınıfla kurulur. Budget ölçümün adını ve sınırını taşır, Overrun aşan örnekleri sayıp dört aşımdan birini verir, countOverruns dört aşımı sabit sırayla sayar.
// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)
class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
val total: Int = samples.size
val over: Int = samples.count { it > budget.limit }
val verdict: String = when {
over == 0 -> "icinde"
over == total -> "hepsinde"
over * 2 < total -> "azinda"
else -> "cogunda"
}
fun line() {
println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
}
}
fun countOverruns(overruns: List<Overrun>) {
for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
println("$name: ${overruns.count { it.verdict == name }}")
}
}
Kaydırılan Liste
Bir listede yirmi öğe olsun, ekrana aynı anda beşi sığsın. Kaydırma her karede listenin ilk görünen öğesini kaydırır. Aşağıdaki yardımcı dosya öğeyi, görünen pencereyi ve en kaba kare işini tanımlar.
// Frames.kt — kaydirilan listede bir karede yeniden olusturulan ogeleri hesaplar
class Item(val key: Int)
fun buildItems(count: Int): List<Item> = List(count) { Item(it) }
fun visible(items: List<Item>, first: Int, window: Int): List<Item> =
items.drop(first).take(window)
fun frameWork(items: List<Item>): Int = items.size
Item bir öğeyi ve ona verilen anahtarı taşır. visible ilk görünen öğeden başlayarak ekrana sığan öğeleri verir. frameWork ise en kaba kuralı kurar: kare, listenin bütün öğelerini yeniden oluşturur. Önce kaydırmanın kendisine bakalım. Sekiz karede ilk görünen öğenin yeri sabit bir diziden gelir: yavaş bir kaydırma, bir duraklama ve ardından hızlı bir kaydırma.
// Main.kt — sekiz karede ekrana giren ogeler
fun main() {
val items = buildItems(20)
val positions = listOf(0, 1, 2, 2, 3, 8, 9, 9)
for ((index, first) in positions.withIndex()) {
val keys = visible(items, first, 5).map { it.key }
println("kare-$index: $keys")
}
}
kare-0: [0, 1, 2, 3, 4] kare-1: [1, 2, 3, 4, 5] kare-2: [2, 3, 4, 5, 6] kare-3: [2, 3, 4, 5, 6] kare-4: [3, 4, 5, 6, 7] kare-5: [8, 9, 10, 11, 12] kare-6: [9, 10, 11, 12, 13] kare-7: [9, 10, 11, 12, 13]
BA1. Kaydırma dizisi modelin girdisidir. Kare-3 duraklamadır, çünkü ilk görünen öğe değişmedi. Kare-5, parmağın hızla sürüklediği bir kaydırmadır: görünen pencere bir kareden ötekine beş öğe atladı, yani önceki karedeki öğelerin hiçbiri ekranda kalmadı. Gerçek bir kaydırmanın bu biçimde olduğu söylenmiyor; dizi, kuralı göstermek için seçildi.
Her Karede Her Şey
Kaba kuralı bütçeyle ölçelim. Kare bütçesi dört öğe olsun. Yirmi öğelik listede her karenin işi yirmi olur, çünkü kare ekranda görünen beşini değil, listenin hepsini yeniden oluşturur. Ölçümü üç liste uzunluğuyla yapıyoruz. Örnekler bir model işlevinin dönüşüdür: her kare için frameWork çağrılır.
// Main.kt — her karede butun ogeler yeniden olusturulur
fun main() {
val positions = listOf(0, 1, 2, 2, 3, 8, 9, 9)
val budget = Budget("kare", 4)
val overruns = listOf(20, 12, 4).map { count ->
val items = buildItems(count)
val samples = positions.map { frameWork(items) }
Overrun("liste-$count", budget, samples)
}
overruns.forEach { it.line() }
countOverruns(overruns)
}
liste-20: butce=kare<=4 | asan=8/8 -> hepsinde liste-12: butce=kare<=4 | asan=8/8 -> hepsinde liste-4: butce=kare<=4 | asan=0/8 -> icinde icinde: 1 azinda: 0 cogunda: 0 hepsinde: 2
Yirmi ve on iki öğelik listelerde sekiz karenin sekizi bütçeyi aştı: hepsinde. Dört öğelik listede hiçbiri aşmadı: içinde. Kod aynıdır ve yalnız listenin uzunluğu değişti. Bunun nedeni modelin kurduğu kuraldır: kare işi ekranda görünen öğe sayısına değil, listedeki öğe sayısına bağlıdır. Liste kısayken kural sezilmez, uzadıkça her kare aynı oranda ağırlaşır.
BA2. Bütçenin dört öğe olması ders kararıdır. Kare bütçesi gerçekte süredir ve kaç öğenin bu süreye sığdığı öğenin ne kadar karmaşık olduğuna, aygıta ve ekranın yenileme hızına bağlıdır. Modeldeki dört öğe bir sığma sınırı iddiası değil, ölçüm biriminin bir seçimidir. Öğelerin hepsinin aynı ağırlıkta olduğu da bir varsayımdır.
Burada hepsinde, kötü bir kodun değil, kuralın görebildiği bir durumun adıdır: her kare bütçeyi aştı. Kural karelerin ne kadar aştığını söylemez. Yirmi öğelik listenin her karesi bütçenin beş katıdır, on iki öğelik listeninki üç katı; ikisi de aynı satıra, hepsindeye düşer. Büyüklüğü görmek için ya frameWork çıktısına bakılır ya da bütçe aşamalı olarak sıkılaştırılır.
Platformda atlanan kareleri üretimde izlemek için bir kare ölçüm kitaplığı kullanılır. Aşağıdaki parça pencerenin her karesini dinler ve atlanan kareleri kaydeder.
// Platform kodu: atlanan kareleri izleme
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.metrics.performance.JankStats
class ListActivity : ComponentActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.createAndTrack(window) { frameData ->
if (frameData.isJank) {
recordJank(frameData.states)
}
}
}
override fun onResume() {
super.onResume()
jankStats.isTrackingEnabled = true
}
override fun onPause() {
super.onPause()
jankStats.isTrackingEnabled = false
}
}
Bu parça platform kodudur ve burada çıktı, süre ya da dosya yolu iddia etmiyoruz. JankStats her kare için bir veri nesnesi verir ve bu nesne o karenin atlanıp atlanmadığını söyler. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Gerçek kitaplık atlanan kareyi süreye göre belirler, modelin adım sayısına göre değil.
Kare Bütçesini Okumak
Kare bütçesi, bir karenin çizilmesi için ayrılan iş miktarının üst sınırıdır. Ekran belirli aralıklarla yeni bir kare ister; o aralıkta hazır olmayan kare atlanır ve kullanıcı bunu takılma olarak görür. Modelde aralık yerine iş adımı sayısını kullanıyoruz, çünkü bu depoda süre ölçmüyoruz. Bütçe, bir karede yapılabilecek en çok iş adımıdır ve kural kaç karenin bu sınırı geçtiğini sayar.
Bu çerçevenin iki sonucu var. Birincisi, tek bir pahalı kare bile aşım sayılır; kullanıcı da çoğu zaman tek bir takılmayı fark eder. İkincisi, kural karelerin sırasını bilmez. Art arda üç kare aşan bir kaydırma ile aralara dağılmış üç aşım, aynı sayıyla raporlanır. Oysa kullanıcı için ilki belirgin bir donma, ikincisi hafif bir pürüzdür.
Yeniden oluşturma bu çerçevede maliyetin kaynağıdır. Bir kare, ekranda görünen her öğeyi baştan oluşturuyorsa, iş öğe sayısıyla büyür. Listede yalnız değişen öğeler yeniden oluşturulursa iş, değişen öğe sayısıyla sınırlı kalır. Kararlı anahtar, hangi öğenin değiştiğini ayırt etmeyi sağlayan kimliktir.
Kaydırma Hızı Neden Önemlidir
Dersin kaydırma dizisinde pencere çoğu karede bir öğe ilerledi, bir karede hiç ilerlemedi, bir karede beş öğe birden atladı. Bu üç durumun maliyeti aynı değildir. Duraklayan karede yeni öğe yoktur; yavaş kaydırmada her kare bir öğe getirir; hızlı kaydırmada pencerenin tamamı yenilenir. Akıllı bir liste ilk durumda neredeyse hiç iş yapmaz, ikincide az, üçüncüde pencere boyu iş yapar.
Bu fark ölçüm tasarımını etkiler. Yalnız yavaş kaydırmayla sınanan bir liste temiz çıkar, hızlı kaydırmayla sınanan aynı liste bütçeyi aşar. Bu yüzden kare örneklerini tek bir kullanım biçiminden değil, kullanıcının gerçekten yaptığı hareketlerin karışımından seçmek gerekir. Modelde karışımı biz yazdık; gerçek bir uygulamada karışımı kullanım kayıtları söyler ve bunu bir model değil yalnız o kayıtlar bilir.
Pencere Dışındaki İş
Tembel liste ekrana sığandan biraz fazlasını önceden hazırlayabilir; kullanıcı kaydırdığında öğe hazırdır. Bu önceden hazırlama işi ortadan kaldırmaz, zamanını kaydırmadan önceye çeker. Kare bütçesi açısından kazanç gerçektir, çünkü iş kaydırma karelerinden çıkar; ama hazırlık da bir yerde ödenir ve o yer bir başka bütçedir. Aynı ilke başlatma dersinde ertelenen iş için de geçerliydi: iş bir bütçeden ötekine taşınabilir ve iki bütçe birlikte okunmadıkça iyileşme tam sayılmaz.
Modelin sınırı burada belirgindir. frameWork yalnız bir karede yeniden oluşturulan öğeleri sayar ve önceden hazırlanan öğeleri hiçbir yere yazmaz. Önceden hazırlamanın etkisini ölçmek isteyen biri modele ikinci bir sayaç eklemelidir; bu derste eklenmedi, çünkü ders tek bir soruya, yeniden oluşturmanın kare bütçesini kaç karede aştığına cevap veriyor.
Ölçümü kare bütçesine bağlamanın bir yararı da hedefin açık olmasıdır. “Liste akıcı olsun” bir dilektir; “yirmi öğelik listede sekiz karenin hiçbiri dört öğeyi aşmasın” ise sınanabilir bir cümledir. Sınanabilir hedef, değişiklik yapıldığında sonucun iyileşip iyileşmediğini tartışmaya yer bırakmadan gösterir. Bütçeyi ve örnek dizisini kodla birlikte saklamak, bu cümlenin zamanla unutulmamasını sağlar.
Kare bütçesini aşan karelerin hangi öğeden geldiğini bulmak ikinci adımdır. İlk adım kaç karenin aştığını bilmektir; ikincisi o karelerin ortak noktasını aramaktır. Bu derste ortak nokta, kaydırma dizisindeki ilk kare ile beş öğelik sıçramaydı ve ikisi de örneklerden okunabildi. Gerçek bir uygulamada bu bilgiyi profil aracının kare başına döküm verdiği çıktı sağlar.
Yaygın Yanılgılar
“Liste kısaysa çizim sorunu yoktur.” Kısa liste bütçenin içinde kalır; ders bunu dört öğelik listeyle gösterdi. Ama bu, aynı kodun uzun listede de bütçeyi tutacağı anlamına gelmez. İş, öğe sayısıyla büyür ve ölçüm yalnız sınadığı boyut için geçerlidir.
“Kararlı anahtar çizimi hızlandırır.” Anahtar işin kendisini azaltmaz; hangi işin tekrarlanmayacağını belirler. Anahtar yanlış seçilirse, örneğin sıra numarası kimlik diye kullanılırsa, öğeler yer değiştirdiğinde aynı öğe farklı kimlikle görünür ve iş yeniden başlar.
“Takılma tek sayıyla özetlenir.” Aşan kare sayısı takılmanın sıklığını verir; büyüklüğünü ve ardışıklığını vermez. Bu yüzden dersin sınırlayıcı ölçümü, iyileşmeyi aşım kategorisiyle değil, kare başına işle birlikte okumayı öğretir.
“Çizim yalnız çizim koduyla ilgilidir.” Bir karenin işi, çizimden önce yapılan hazırlıkları da içerir: veri dönüşümü, yerleşim hesabı, görünür öğelerin belirlenmesi. Modelde bunların hepsi frameWork içinde toplandı; gerçek uygulamada hangi parçanın pahalı olduğunu profil aracı söyler.
Sınırlayıcı Ölçüm
Kaba kuralın çaresi, her öğeye kararlı bir anahtar vermek ve yalnız anahtarı değişen ya da yeni görünen öğeyi yeniden oluşturmaktır. Kaydırılan bir listede bir öğe ekrandan çıkıp girmedikçe aynı anahtarla tanınır; platform onun içeriğini yeniden kurmak zorunda kalmaz. Aşağıdaki parça bunun platformdaki karşılığıdır.
// Platform kodu: kararli anahtarli kaydirilan liste
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
@Composable
fun MessageList(messages: List<Message>) {
LazyColumn {
items(messages, key = { it.id }) { message ->
MessageRow(message)
}
}
}
Tembel liste yalnız ekrana sığan öğeleri oluşturur ve anahtar her öğeye değişmeyen bir kimlik verir. Modelde bunu şöyle kuruyoruz: bir karenin işi, önceki karede görünmeyen öğelerin sayısıdır. İlk karede önceki kare yoktur, bu yüzden pencerenin hepsi yeniden oluşturulur.
// Main.kt — kararli anahtarla yalniz ekrana giren ogeler yeniden olusturulur
fun keyedWork(previous: List<Item>, current: List<Item>): Int {
val seen = previous.map { it.key }.toSet()
return current.count { it.key !in seen }
}
fun main() {
val items = buildItems(20)
val positions = listOf(0, 1, 2, 2, 3, 8, 9, 9)
val windows = positions.map { visible(items, it, 5) }
val samples = windows.mapIndexed { index, now ->
keyedWork(if (index == 0) emptyList() else windows[index - 1], now)
}
println("is: $samples")
val overrun = Overrun("anahtarli", Budget("kare", 4), samples)
overrun.line()
countOverruns(listOf(overrun))
}
is: [5, 1, 1, 0, 1, 5, 1, 0] anahtarli: butce=kare<=4 | asan=2/8 -> azinda icinde: 0 azinda: 1 cogunda: 0 hepsinde: 0
Aynı kaydırma, aynı bütçe ve aynı yirmi öğe: aşım hepsindeden azındaya indi. Yeniden oluşturulan iş yirmiden bire, bazen sıfıra düştü. Ama iki kare hâlâ bütçeyi aşıyor. Birincisi ilk karedir; liste ilk kez kurulurken beş öğe de yenidir. İkincisi hızlı kaydırmanın beşinci karesidir; pencere beş öğe birden atladığı için beşi de yeni sayılır. Anahtar, yeniden oluşturmayı ekrana giren öğelere daraltır ama giren öğe sayısı bütçeden büyükse kareyi kurtarmaz.
BA3. Modeldeki “yeni görünen öğe” ile gerçek bir listedeki yeniden oluşturma birebir aynı değildir. Gerçekte bir öğenin içeriği değişirse, anahtarı aynı kalsa bile yeniden oluşturulur; modelde içerik değişmez. Anahtar yalnız bir öğenin kimliğini koruyarak gereksiz yeniden oluşturmayı önler. Kural bu farkı da görmez.
Kuralın görmediği şeyi açıkça söyleyelim. Hepsinde ile azında arasındaki fark, kuralın gözünde bir sınıf farkıdır, ama iş açısından çok daha büyüktür: toplam iş yirmi öğe çarpı sekiz karedir, yani yüz altmış öğeden on dörde indi. Kural bu oranı vermez, yalnız aşan kare sayısını verir. Aşımın büyüklüğünü görmek istiyorsak örneklerin kendisine, yani is: satırına bakarız. Kural nedeni de söylemez: ilk karedeki aşım liste kurulumundan, beşinci karedeki aşım hızlı kaydırmadan gelir ve ikisi aynı sayıda aşım olarak sayılır.
Özet
- Bu dersin aşımı: hepsinde. Yirmi öğelik listede her kare bütün öğeleri yeniden oluşturdu ve sekiz karenin sekizi dört öğelik bütçeyi aştı.
- Kare bütçesi gerçekte süredir; bu derste aynı birimde bir iş sayısı olarak, kare başına yeniden oluşturulan öğe diye ölçüldü ve süre yazılmadı.
- Aynı kod, dört öğelik listede içinde, on iki ve yirmi öğelikte hepsinde çıktı: kaba kuralda kare işi görünen öğeye değil, liste uzunluğuna bağlıdır.
- Kararlı anahtar yeniden oluşturmayı ekrana giren öğelerle sınırladı ve aşımı hepsinde sınıfından azında sınıfına indirdi. Kalan iki aşım ilk kare ile beş öğelik sıçramaydı.
- Kural aşan kare sayısını görür; aşımın büyüklüğünü ve nedenini görmez.
Sonraki Adım
Kaydırma akıcı olsa bile her kare belleğe yeni nesneler bırakır. Bazıları kullanıldıktan sonra serbest kalmaz ve ekran kapansa da bellekte durur. Sıradaki ders, Bellek Yönetimi, kaydı kaldırılmayan bir dinleyicinin her açma ve kapama döngüsünde bir ekranı tutmasının tutulan nesne sayısını kaç döngüde bütçenin üstüne çıkardığını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.