Android UI Nasıl Çizilir?
Ekrandaki Her Piksel Bir Kararın Sonucudur
Bir butona bakıyorsunuz. Köşeleri yuvarlatılmış, gölgesi var, üzerinde metin yazıyor, dokunduğunuzda rengi değişiyor. Bunların tamamı olağan görünür. Ama bu görüntünün ekrana ulaşması için arka planda son derece koordineli bir süreç işliyor.
Android UI'ı nasıl çiziyor? Bir View nasıl ekranda beliriyor? Ve bu sürecin hızlı ya da yavaş olmasını ne belirliyor?
Her Şey View Hiyerarşisiyle Başlar
Android UI'ının temel yapı taşı View'dur. Button, TextView, ImageView, RecyclerView — bunların tamamı View sınıfından türer. Ve bu View'lar ekranda düz bir liste olarak değil, ağaç yapısında organize edilmiş bir hiyerarşi olarak bulunur.
En üstte bir kök View vardır. Onun altında ViewGroup'lar, ViewGroup'ların altında diğer View'lar yer alır. Bu ağaç ne kadar derin ve geniş olursa çizim süreci o kadar karmaşık hale gelir.
Her şey bu hiyerarşi üzerinde gerçekleşir.
Üç Aşamalı Çizim Döngüsü
Android bir View'ı ekrana çizmek için üç temel aşamayı sırayla gerçekleştirir.
Measure aşaması her View'ın ne kadar yer kaplayacağını hesaplar. Üst View, alt View'lara "şu kadar alanın içinde sığmak zorundasın" der. Alt View bu kısıtı değerlendirir ve kendi boyutunu bildirir. Bu diyalog ağacın tepesinden tabanına, sonra tekrar tepesine doğru ilerler. Görece basit görünen bu hesap, iç içe geçmiş karmaşık layout'larda onlarca kez tekrarlanabilir.
Layout aşaması her View'ın tam olarak nereye yerleştirileceğini belirler. Measure aşamasında boyutlar netleştikten sonra her View'ın sol üst koordinatı hesaplanır. ViewGroup kendi alt View'larının konumlarını bu aşamada kesinleştirir.
Draw aşaması ise gerçek çizimin yapıldığı adımdır. Her View, kendisine ait bir Canvas nesnesi üzerinde ne çizmesi gerekiyorsa çizer. Arka plan rengi, sınır çizgileri, metin, görsel — bunların tamamı bu aşamada Canvas'a işlenir.
Canvas ve Paint: Çizimin Araçları
Draw aşamasında her View bir Canvas nesnesi alır. Canvas, çizim komutlarının gönderildiği soyut bir yüzeydir. View bu yüzeye çizgi, dikdörtgen, metin, yay gibi temel şekilleri çizer.
Paint nesnesi ise bu çizimler için görsel parametreleri taşır. Renk, şeffaflık, yazı tipi, kenar yuvarlama — bunların tamamı Paint içinde tanımlanır. Her çizim komutu bir Canvas ve bir Paint alır.
Canvas üzerine yazılan komutlar doğrudan ekrana gitmez. Önce bir ara tampon belleğe — bitmap buffer'a — işlenir. Ardından bu buffer ekrana gönderilmek üzere sıraya alınır.
Invalidate: Yeniden Çizim Talebi
View'lar sürekli yeniden çizilmez. Bir View'ın görünümü değişmediği sürece sistem onu tekrar çizmez — bu hem CPU hem de GPU açısından büyük bir tasarruf sağlar.
Bir View'ın yeniden çizilmesi gerektiğinde invalidate() metodu çağrılır. Bu çağrı sisteme "bu View'ın çıktısı değişti, bir sonraki frame'de yeniden çiz" mesajını verir.
Bir TextView'ın metni güncellendiğinde, bir butonun rengi değiştiğinde, bir animasyon karesi geçtiğinde — bunların hepsinin arkasında bir invalidate() çağrısı yatar.
Choreographer: Zamanlamayı Yöneten Orkestra Şefi
Tüm bu çizim işlemlerinin ne zaman gerçekleşeceğini Choreographer belirler.
Choreographer, ekranın yenileme sinyaliyle — VSync — senkronize çalışır. Çoğu cihazda ekran saniyede 60 kez yenilenir. Her yenileme için Choreographer bir "frame başlıyor" sinyali yayınlar. Bekleyen tüm View güncellemeleri ve animasyonlar bu sinyal geldiğinde işlenir.
Bu senkronizasyon kritik öneme sahiptir. Çizim ekranın yenileme döngüsüyle uyumlu olmazsa yırtılma (tearing) ya da atlanmış frame (dropped frame) görüntülenebilir. Choreographer bu uyumu garanti altına alır.
Her frame için ayrılan süre 16 milisaniyedir. Measure, Layout ve Draw aşamalarının tamamı bu süre içinde bitmek zorundadır. Süre aşılırsa frame atlanır ve kullanıcı takılma hisseder.
RenderThread: İşi Paylaşmak
Android 5.0'dan itibaren çizim işi iki thread arasında paylaştırıldı.
Main thread Measure ve Layout hesaplarını yapar, çizim komutlarını hazırlar ve bunları bir display list'e dönüştürür. Display list, bir View'ın nasıl çizileceğini açıklayan komut serisidir.
RenderThread bu display list'i alır ve GPU'ya iletir. Bu ayrım sayesinde main thread bir sonraki frame'in hesaplarını yaparken RenderThread önceki frame'i GPU'ya yollamaya devam edebilir. İki iş paralel ilerler, toplam süre kısalır.
Geliştirici Perspektifinden Bakış
Bu süreci anlamak doğrudan performans kararlarınızı etkiler.
Derin View hiyerarşileri Measure aşamasını ağırlaştırır. Bir layout'u flatten etmek — yani gereksiz iç içe geçmeyi ortadan kaldırmak — bu hesabı hızlandırır. ConstraintLayout bu amaçla tasarlanmıştır: tek bir katmanda karmaşık layout'lar ifade etmenizi sağlar.
onDraw metodunda nesne oluşturmaktan kaçının. Her frame'de yeni Paint ya da Path nesneleri yaratmak GC'yi meşgul eder. Bu nesneler sınıf değişkeni olarak önceden oluşturulmalı ve yeniden kullanılmalıdır.
Ve invalidate() çağrılarını bilinçli kullanın. Gereksiz yere büyük alanları geçersiz kılmak, ihtiyaçtan fazla çizim işlemi tetikler.