Android uygulaması geliştiren herkes için uygulama performansı hep bir sorun olmuştur. Çünkü aslında uygulama yavaş dendiğinde, başlangıç için yapılması gereken ilk önce profiling tool’ları kullanıp veri toplamak ve sonrasında topladığımız veriyi inceleyerek sorunu tespit edip düzeltmektir. Süreç biraz can sıkıcı olmakla beraber bazen de görselliğimizi istediğimiz seviyeye çekmeye çalışırken, bazı küçük noktaları kaçırıp aslında çok da güçlü olmayan cihazlarda – ki kullanıcıları çok daha fazla- uygulamanın ekranı, bazen cihazı kitlemesine kadar sorunlara yol açabiliyoruz.

Renderingle başlayarak, bu seride basit tricklerle nasıl daha performanslı uygulama yazabiliriz, profiling tool’ları nasıl kullanabiliriz üzerinde genel bilgi vereceğim.

Step 1: Rendering

Renderingle ilgili yaşanan problemler aslında en sık yaşanan performans problemidir. Tasarımcılar, kullanıcılar için en kullanışlı deneyimi sağlayacak şekilde uygulamanın geliştirilmesini isterken aslında diğer tarafta tüm o şaşalı grafikler, animasyonlar her cihazda efektif çalışmayabilirler.

Screen Shot 2015-11-08 at 15.01.42

İlk olarak, sistem her 16 ms’de bir Activity’yi redraw etmeye çalışacaktır. Aslında bu da şu anlama gelmektedir. Ekranı update etmek için yapmaya çalıştığınız tüm logic bu 16 ms’lik periyodda yapılması gerekmektedir yani 60 fps.

Frame per second(fps) değeri cihazın donanımıyla alakalı bir rakamdır ki (1000 ms / 60 hz = 16,666 ms/frame) 1 sn’de ekranın kendini ne kadar hızlı update edeceğini ifade eder. Çoğu cihaz 60 hertz civarında refresh edecektir bu da her bir frame için çalışacak drawing logic’in 60 ms’de tamamlanması gerektiği anlamına gelir.

Eğer bu window’u kaçırsanız yani mesela calculationınız 24 ms sürerse, o zaman dropped frame denilen durumu yaşamış olursunuz. Sistem ekrana yeni bir resim çizmeye çalıştı ama aslında hazır değildi, o yüzden hiç bir şeyi refresh etmedi. Bu da aslında kullanıcının refresh edilmiş grafiği 16 ms değil 32 ms sonra görmesine sebep oldu. Bir tane bile dropped frame olduğunda ise animasyon artık smooth gözükmemeye başlayacaktır. Çok sayıda dropped frame olması durumunda ise, kullanıcıya ekran takılıyormuş gibi bir görünüm vermiş olursunuz. Bu da aslında kullanıcıların uygulamanızla ilgili şikayet etmeye başladığı noktadır.

Screen Shot 2015-11-08 at 15.32.39

Renderingi iki ana dala ayrılıyor olarak düşünebiliriz: CPU ve GPU. Imajların ekrana çizilebilmesi için CPU ve GPU beraber çalışmaktadır. Her ikisinin de ayrı ayrı spesifik tanımlanmış ve belli kurallara göre çalışması gereken processleri vardır.

CPU:

Measure –> Layout –> Record –> Execute

GPU:

Rasterization

CPU tarafında en yaygın performans problemi, gereksiz layout kullanımlarından ve invalidatelerden gelmektedir ki bunlar sizin view hiyeraşinizin ölçümünün, yıkımının ve yeniden inşa edilmesinin zorunluluğu anlamına gelmektedir. Invalidation’la gelen bu problem genellikle 2 şekilde oluşur. Bunlardan biri view’inizin frame süresi boyunca çok fazla rebuilt edilmesidir. Diğeri ise view hiyerarşinizin, çok fazla kısmını(gerekmeyen kısımlarını da)  gereksiz bir şekilde invalidate, redraw ettiğiniz için çok fazla zaman harcamanızdır.  Örneğin displaylistlerin refresh edilmesini ele alırsak, bir listenin refresh edilmesinde bu iki problamatik durum da hem CPU overhead olması  hem de GPU kaynaklarının gereksiz kullanımı olarak bize yansır.

2. önemli problem ise GPU tarafında, “overdraw”dur ki bu da aslında GPU zamanını pixelleri tekrar tekrar boyayarak boşa harcamamızdır.

Screen Shot 2015-11-08 at 16.29.19

Peki o zaman, Activity’niz aslında ekrana nasıl çizilir? Ya da şöyle dersek, xml’ler ya da markup language kullanıcının anlayacağı ve göreceği şekilde nasıl piksellere dönüşür?

İşlem olarak bu aşama rasterization olarak tanımlanmaktadır. Bu işlem aslında String gibi, Button gibi objelerin ekranınızda piksellere dönüşme Screen Shot 2015-11-08 at 18.32.14işlemidir ve bu işlem gerçekten zaman alan bir işlemdir. Mobil cihazlarınızda bulunan küçük bir parça, bu işlemin daha hızlı gerçekleşmesi için design edilmiştir. Hepinizin bildiği gibi – GPU.

GPU, bazı spesifik primitive’lerin(poligonlar, texture, image,  etc.) kullanımı için tasarlanmıştır. Bu primitive’ler ise GPU ekrana herhangi bir şey çizmeden önce CPU tarafından sağlanır. Android’de bu işlemin yapılmasını sağlayan API OpenGL ES’dir. Her zaman sizin Button, CheckBox, Path gibi UI objeleriniz ekrana çizilmesi gerektiğinde aslında ilk önce CPU’da poligonlara ve texture’lara çevirilir ve ardından rasterize işlemi için GPU’ya iletilir.

Hem CPU’da hem GPU’da yapılan işlemler çok da hızlı yapılabilen işlemler değildir. Ama OpenGL ES API sayesinde content bir kere GPU’ya upload edildiğinde eğer değişiklik yapılmayacaksa örneğin button’a GPU memory üzerinden erişim sağlanabilir. Uygulamamızı daha hızlı yapmak için değişiklik yapmak zorunda olmadığımız durumlarda mümkün olduğunca uzun süre, sadece referansını kullanarak işlem yapıp görsel değişiklik yapmamamız gerekir. Çünkü GPU’da yer alan bir resource’u her update ettiğimizde işlem süresini kaybediyor oluruz.

Honeycomb’dan(Android 3.0) beri, tüm UI rendering sistemi GPU’yla çalışmaktadır. Sonrasında çıkan release’lerde ise bu performans daha da arttırılmıştır.  Android sistemi aslında biz farkında olmadan bir çok işin kısalması, reuse edilebilmesi, GPU resource’larının recycle edilebilmesi için arkada çok fazla iş yapar. Örneğin temanız tarafından sağlanan tüm resource’lar, bitmapler, drawable’lar, etc. tek bir texture içine gruplanır ve GPU’ya upload edilir. Yani aslında bunlarla ilgili bir işlem yapmamız gerektiğinde bizim yapmamız gereken herhangi bir conversion bulunmamaktadır.Gerçekten hızlı display edileceklerdir. Text’lerin draw edilmesi ise tamamen farklı bir süreçten geçmektedir. İlk olarak CPU’ya image olarak draw edilmektedir, ardından ise GPU’ya yüklenmekte ve string’deki her bir karakter için ekranınızda rectangle’a draw edilmektedir. Ve aslında tüm bunlar biz özel birşey yapmadığımız sürece de Android’in kendi sistemi tarafından handle edilmektedir.

Overdraw: Overdraw, ekranınızda yer alan bir pixel’in tek bir frame’de kaç kere yeniden çizildiği olarak tanımlanabilir.

Overdraw, gerçek bir problemdir çünkü aslında biz pikselleri her yeniden render ettiğimizde, ekranın son halini veremiyor oluruz ki bu da GPU performansının boşa harcanmasıdır. Bir diğer yandan da modern layoutları kullanırken yaşanması çok olası olan bir sorundur. Aslında daha güzel bir görünüm kullanıcıya sunmaya çalışırken view’leri istiflediğimizde, layerlar halinde kullandığımızda bu sorunu yaşama olasılığımızı da arttırmış oluyoruz. Uygulamanın performansını maksimize etmek için overdrawları minimize etmeliyiz. gettingstarted_image01

Bu konuda Android’in Developer Options menusu altında yer alan Debug GPU Overdraw özelliği aslında overdraw yapılan yerlerin görülebilmesi için bize yardımcı olmaktadır.

gettingstarted_image02

Bu noktada panik olmanıza gerek yok, cihazınızın görünümü tam olarak yanda görünen gibi olacaktır ki bu tamamen normal. Android overdraw edilmiş alanların gösterimi için farklı renkler kullanmaktadır.

gettingstarted_image03

  • True color: No overdraw
  • Blue: Bir kere overdrawn
  • Green: 2 kere overdrawn
  • Pink: 3 kere overdrawn
  • Red: 4 ya da daha fazla overdrawn

Aslında maviler kabul edilebilir olanlardır, kırmızılardan mavilere nasıl geçeceğimiz konusunda ise iki temel yol vardır.

İlk olarak, gereksiz olan aslında sonuçta oluşacak olan görünüme hiç bir katkısı olmayan backgroundları ve drawableları kaldırmalısınız.

İkinci olarak ise, ekranınızda  view’inizin bir kısmını hide eden bildiğiniz view’leri tanımlayabilirsiniz. Bu size CPU ve GPU overhead’in azalması olarak geri dönecektir.

gettingstarted_image04

Clipping: Android sistemi overdrawın bir problem olduğunu bildiği için son görünümde aslında invisible olan view’leri UI’a çizmekten kaçınmaktadır. Bu clipping olarak tanımlanmaktadır ve Android’in en önemli performans optimizasyonlarından da biri olarak bilinmektedir.

Screen Shot 2015-11-08 at 19.40.33Bir diğer yandan ise kendi yazdığımız onDraw’u override eden kompleks custom view’lerde bu teknik çalışmamaktadır. Bu gibi durumlarda aslında sistem bizim view’i nasıl çizdiğimizi bilmediği için hidden olan view’leri kaldırmak zordur.

Mesela yandaki üst üste dizili olan kartlarda aslında sadece en üstteki tamamen gözükmektedir. Diğerlerinin yanı gözükmekte. Bu gibi durumlarda tüm kartları tamamen çizmek gerçek bir zaman kaybıdır. Fakat burada Canvas classınızda bazı özel methodlarla Android framework’üne canvasın hangi kısmının hidden olduğunu ve çizilmesine gerek olmadığını söyleyebiliriz.

Bu durumda en kullanışlı method canvas’ın clipRect methodudur ki verilen view için drawable sınırlarının belirlenmesini sağlar. Yani bu sınırların dışında yapılan çizim işlemleri ignore edilebilir. clipRect API sistemin drawing’den nasıl kaçınabileceğini bilmesinde yardımcı olur. Ama bunun dışında bizlerin de customview’lerde yapabileceğimiz başka şeyler var. Örneğin, neyi çizeceğimizi bilirsek, onun clipping rectangle’ın içinde mi dışında mı olduğunu bu sisteme gerçekten yardımcı olacaktır.

Fakat tüm bu işlemleri de kendimiz yapmak zorunda değiliz. Canvas.quickReject() methodu, verilen alanın tamamen çizim alanı dışında olup olmadığını kontrol eder ve eğer öyleyse tüm çizim işlemini atlamış oluruz.

Şimdiye kadar anlattıklarım GPU tarafındaki performansı arttırmak üzerineydi. CPU’yla devam ediyorum. Screen Shot 2015-11-08 at 20.18.32

Ekrana herhangi bir şey çizebilmek için, Android’in öncellikli olarak, high-level XML’in GPU’nun kabul edip, render edebileceği bir hale getirmesi gerekmektedir. Bu işlem internal bir Android objesi olan display list’le yapılmaktadır. Display list temel olarak, bir view’i GPU’da render edebilmek için gerekli tüm bilgiyi tutar. Bu bilgiler GPU’nun ihtiyacı olabilecek tüm assetler, render edebilmek için OpenGL’le çalıştırmak için gerekli komutları listesini kapsamaktadır.

Bir view ilk defa render edileceği zaman, onun için bir display list oluşturulur. Ve bu view’i ekrana render edeceğimiz zaman tüm çizim komutlarının GPU’ya submit edilebilmesi için ise bu display listi çalıştırırız. Eğer bu view’i ilerde tekrar render etmemiz gerekirse -mesela ekrandaki position’ı değişmiş olabilir- ihtiyacımız olan bu display listi tekrar çalıştırmaktır. Ama eğer ilerde bu view’in görsel kısımlarında değişiklik olursa, bu durumda view’i update edebilmek için bir önceki display list artık geçerli değildir ve  bu durumda display list’i yeniden yaratılır ve yeniden çalıştırılır.

Burada unutulmaması gereken, view’de yapılan her görsel değişiklikte display list tekrar create edilecek ve tekrar çalıştırılacaktır. Bu işlemin performansı ise view’inizin ne kadar kompleks olduğuna bağlı olarak değişecektir.

Görsel değişikliğin tipine bağlı olarak, rendering işlemlerinde başka view’leri etkileyen durumlar da oluşabilir. Mesela, aşağıdaki görselde yer alan button size olarak iki katına çıktığında aslında altında yer alan textview’in kaymasına ve parent view’in size’ının artması gibi sonuçlara yol açmaktadır. Bu tip görsel değişiklikler, renderingde fazladan aşamalara ihtiyaç oluşturur.

Screen Shot 2015-11-08 at 20.35.46

View’inizin size’ı değiştiğinde, measure aşaması tetiklenecektir, ve hiyerarşide devam ettiğimizde her bir view’e yeni size’larının ne olduğu sorulacak. Aslında bu işlem view’in size’ı her değiştiğinde tekrarlanır. Örnek olarak; padding, drawable size, text’nin scale’i, width, height, vb.

Eğer view’lerin position’ı değiştirilirse, yada requestLayout çağrılırsa, yada bir viewgroup içindeki view’leri yeniden konumlandırırsa, bu durumda da layout aşaması tetiklenecektir.

Aslında Android sistem olarak, bu aşamaları işletmede oldukça başarılır. Ama eğer biz custom view’lerde olması gerekenin dışında bir şeyler yaparsak, ya da tonlarca view’i aynı anda çizdirmeye çalışırsak, bu gibi durumlarda frame time’ı hesaba katamayabilir.

Measure ve Layout aşamaları aslında gerçekten çok performanslı çalışan steplerdir ancak, eğer sizin view hiyerarşininiz kontrolden çıkarsa da aslında performans problemi yaratmaya eğilimli olular.

Bu konuda Hierarchy Viewer size yardımcı olacak. Hierarchy Viewer sizin tüm UI gettingstarted_image001yapınızı canlandırmanızı sağlayacak ve aynı zamanda yapınızda farklı view’lerin rendering performansını anlamanızı sağlayacaktır.

Hierarchy Viewer’ı kullanabilmeniz için cihazınızda Developer Options’ın açık olması ve gerçek bir device kullanmanız gerekmektedir. ANDROID_HVPROTO ortam değişkeninin PC’nizde set edilmiş olması gerekmektedir. Nasıl set edileceğiniz ilgili linkte bulabilirsiniz. Yalnız cihazınız eğer Developer Cihazı değilse environment variable’ı set etmek yeterli olmayacaktır. Diğer cihazlar için linkteki workaroundu kullanarak, Hierarchy Viewer’ı kullanabilirsiniz. Source kod olarak ise kendi projenizi kullanabilir ya da developer.android’de de link verilen Project Sunshine’dan faydalanabilirsiniz.

Hierarchy Viewer’ın size ne gibi bilgiler verdiğine gelirsek;

  • Tree Overview görünümü size view hiyerarşinizin kuş bakışı görünümünü görmenizi sağlar.
  • View’in detaylarını görmek için nodelardan birine tıklayabilirsiniz.
  • View Properties tabına tıklayarak selected view node’un özelliklerini görebilirsiniz.
  • İlgili view node’a çift tıklayarak popup’da nasıl göründüğüne bakabilirsiniz.
  • View id nizi ve view type’ınızı da node’da görebilirsiniz.

Screen Shot 2015-11-08 at 22.47.43

Hierarchy Viewer’daki nodelardan tıklayarak source koda gidemezsiniz çünkü aslında gördüğünüz uygulama sayfasıdır ancak node’ların üzerindeki id’ler view’lerin kendi idleridir ve bunlardan faydalanıp kodunuzu inceleyebilirsiniz.

  1. Hierarchy Viewer yapınızın Android perspektifinden nasıl göründüğünü anlamanızı sağlayacaktır.
  2. Gereksiz view’leriniz varsa bunu görmenizi ya da yapınızı daha flatleştirmenize olanak sağlayacaktır ve rendering performansını arttırmanıza yardımcı olacaktır.

gettingstarted_image014

Profiling with Hierarchy Viewer:

Ona bağlı olan viewleri incelemek istediğiniz root nodu tıkladıktan sonra yandaki resimde yer alan profiling butonunu tıkladığınızda, sub nodeların altında 3’er tane farklı renklerde daire göreceksiniz. Yeşil – Kırmızı – Sarı

En soldaki nokta, rendering işleminde draw processini göstermektedir. Ortada bulunan nokta layout sonuncusu ise execute işlemini gösterir. Bunlar da aşağıdaki görsel de göreceğiniz üzere Measure, Layout ve Create Display List’e denk gelmektedir.gettingstarted_image015

  • Yeşil : Seçili view diğer view’lerin en az yarısından daha hızlı render edilmektedir.
  • Sarı :Seçili view diğer view’lerin yarısından daha azından daha hızlı render edilmektedir.
  • Kırmızı :Seçili view diğer view’lerin en yavaş yarısından içindedir.

Hierarchy Viewer ilişkili performansı çıkardığı için her zaman kırmızı noktalar olacaktır. Bu kullanıcıya o view’ın sorunlu çok yavaş olduğunu göstermez.

Kırmızı nokta uygulamanızda potansiyel bir problemi gösteriyor olabilir. İlişkili olarak çıkarılan performans da mutlaka kırmızı noktalar olacaktır. Sizin sadece bunların zaten kırmızı olmasını beklediğiniz node’lar olup olmadığını kontrol etmeniz gerekmektedir. Fakat eğer root node’a bağlı 20+ node varsa ve hepsi kırmızıysa o zaman gidip onDraw methodunu kontrol etmeniz gerekir.

Tips&Tricks:

  • getWindow().setBackgroundDrawable(null); // Temadan aldığı renkle aynıysa bu gereksiz
  • android:background:”@null” // xml
  • Canvas.clipRect();
  • Canvas.quickReject();
  • Hiyerarşinizi mümkün olduğunca flat kurmaya çalışın
  • Gereksiz layoutlardan kaçının
  • LinearLayout yerine RelativeLayout kullanımı inner layout kullanımını azaltabilir, seçeneklerinizi değerlendirin. Rendering işleminde RelativeLayout LinearLayout’a göre daha hızlıdır.

Kaynaklar: