Android App Startup Time-Performance

Kerim Fırat
10 min readSep 24, 2022

Yazılım ve donanımın her aşamasında “performans” konusu hayati derecede önemlidir. Özellikle mobil uygulamalarda kullanıcının beklentisi çok daha yüksektir ve bu konuda tahammülü oldukça sınırlıdır.
Hatta bazı ürün-uygulamalarda kullanıcının tahammül sınırı çok düşüktür. Google Play’a koyduğunuz uygulamanızın yarım saniye geç açılması/yavaş olması,kullanıcının sadece uygulamanızı terketmekle kalmayıp,Play’daki puanlamayı da buna göre etkileyeceğini biliyoruz.
Bu nedenle uygulamanın stabil ve özellikle performanslı olması için firmalar ciddi kaynak-bütçe tüketiyorlar.
Kullanıcı gözünden performans kavramını ele aldığımızda elbette uygulamanın ilk başlangıç süresi ciddi ve oldukça önemli bir aşamadır. Eğer uygulamanız hızlı bir şekilde start oluyor-başlatılıyor ise,bu bağlamda kullanıcı gözünden artı puan alarak ürünümüzü sunmuş oluruz. Dolayısıyla uygulamanın ilk start süreci çok daha kritiktir ve performans geliştirmeleri bu noktadan başlar.
Ancak her ne kadar performans üzerinde çalışsak bile bu belli bir sınırda kalabilir. Özellikle uygulamanın başlatma(startup) süreci bu anlamda sınırlı kalması yaygın bir durumdur.
Dolayısıyla özellikle uygulamanın başlatılma aşaması iki noktaya ayrılır. Birinci nokta “süre(time)”,ikinci nokta “performans” olur. Elbette bu iki nokta aslında tek noktada birleşir ancak durumu ele alırken bunlar ayrı tutulmalıdır. Bu makalede “Startup time”,dolayısıyla “Startup time-performance” incelenecektir. Yani uygulamanın start olma süreci ele alınacaktır. Bunun için biraz Android OS(AOSP) mimarisine de göz atarak ilerleyeceğiz.

App Startup Durumları

Android’de uygulama başlatma süreci üç durumdan birinde gerçekleşebilir. Bunlardan ilki “Cold Start”,ikincisi “Warm Start” ve üçüncüsü “Hot Start” dır. Cold Start’da uygulamanız sıfırdan başlar. Diğer durumlarda ise işletim sisteminin uygulamanızı arka plandan ön plana getirmesi gerekir. Optimizasyon-geliştirme yapılırken her zaman “Cold Start” baz alınmalıdır. Ve bu saydığımız üç durum süreçlerini ve bu süreçlerde ne tür etkileşimler olduğunu iyi bilmemiz gerekir.

Cold Start

Bu başlatma sürecinde uygulamanız sıfırdan başlatılır. Bir uygulamanın sıfırdan başlatılması için çeşitli nedenler vardır. Kullanıcı uygulamayı ilk kez yüklemiştir. Veya işletim sistemi uygulamayı tamamen sonlandırmıştır. Veya işletim sistemi-cihaz yeniden başlatılmıştır. Bu gibi durumlarda uygulama sıfırdan,yani “Cold Start” olarak başlatılır. Optimizasyon yapılırken bu süreyi en aza indirmek oldukça zahmetlidir. Bu süreçte işletim sisteminin üç adet görevi vardır. Bu görevler şunlardır:
1.Uygulamayı yükleme ve başlatma.
2.Başlatıldıktan hemen sonra uygulama için boş bir başlangıç penceresi görüntülenir(Henüz UI draw edilmemiştir).
3.Uygulama process oluşturmak.

İşletim sistemi uygulamaya ait bir “Process” oluşturur oluşturmaz,ilgili “Process” sonraki aşamalardan sorumlu hale gelir.
Bu aşamalar:
1.Uygulama nesnesini oluşturma.
2.Main Thread başlatılması.
3.Main Activity oluşturulması.
4.Inflating.
5.Ekranı döşemek. Layout çizim için yüklenir.
6.Draw(Çizim) sürecinin başlatılması.

Son aşama,yani 6.aşama gerçekleştikten sonra,sistem o anda görüntülenen arka plan penceresini Main Activity ile değiştirir. Bu noktada kullanıcı uygulamayı kullanmaya başlayabilir.
Aşağıdaki görselde “process” oluştulması ve devamındaki süreç gösteriliyor.

Uygulamanın oluşturulması ve Activity’nin oluşturulması sırasında performans sorunları ortaya çıkabilir.

Uygulama Oluşturma:
Uygulama başlatıldığında,sistem uygulamayı ekrana çizmeyi bitirene kadar boş bir başlangıç penceresi ekranda kalır.Bu noktada sistem process,uygulamanın başlangıç penceresini değiştirerek kullanıcının uygulamayla etkileşime girmesine izin verir. Bu noktadan itibaren sistem ve uygulama düzeyindeki süreçler “uygulama yaşam döngüsü(App Lifecycle)” aşamalarına uygun olarak ilerler.

Activity Oluşturma:
Uygulama process activity’i oluşturduktan sonra ilgili activity aşağıdaki işlemleri gerçekleştirir.
1.init “values”
2.Constructor çağrısı.
3.Callback çağrıları(Activity.onCreate gibi).

Warm Start

Bu başlatma sürecinde uygulama belli bir seviyede,yani “Cold Start” sırasında gerçekleşen işlemlerin bazı alt kümelerini kapsar. Aynı zamanda “Hot Start”’dan daha fazla yükü temsil eder.
Aynı zamanda “Warm Start” olarak kabul edilebilecek birçok potansiyel durum vardır.
Örneğin:
* Kullanıcı uygulamadan çıkmıştır ancak ardından yeniden başlatmıştır. Uygulama process’i çalışmaya devam etmiş olabilir ancak uygulamaya ait “onCreate()” çağrısı Activity’i yenieden oluşturması gerekir. App Lifecycle’a baktığımızda bu süreci görebiliriz.
* Sistem uygulamayı bellekten çıkarır ve ardından kullanıcı yeniden başlatır.Dolayısıyla process ve activity yeniden başlatılması gerekir. Ancak task “SaveInstance”’dan yararlanarak devam edebilir.

Hot Start

Uygulamanın bu süreçte başlatılması “Cold Start” ve “Warm Start”’dan çok daha basit ve düşük maliyetlidir. Hot Start’da sistemin tek yaptığı Activity’i ön plana çıkarmaktır.
Dolayısıyla uygulamaya ait activity’ler vb. bellektedir ve sistemin nesne başlatma,layout,draw vb. süreçlerini baştan almaya gerek yoktur.Yani “lifecycle” üzerinde “onResume()” buna karşılık gelir.
Ancak “onTrimMemory()” gibi süreçler gerçekleşmişse bellekte temizlik yapılmış demektir. Bu durumda uygulamaya ait nesnelerin yeniden oluşturulması gerekir. Dolayısıyla süreç “Hot Start” olarak kalsa da,uygulama “lifecycle” üzerinde bakıldığında biraz geriye gidilir. Dolayısıyla “Hot Start” ile “Cold Start” aynı ekran davranışını gösterir.

Aşağıdaki görselde başlatma durumlarını ve bunların ilgili işlemlerini gösterir.

Processes Manager

Android işletim sistemi,kullanıcıyla etkileşimde olan,yani ekranda açık olan uygulamalara öncelik verir. Ancak uygulamalar arka plana alındığında durum değişir. Arka planda çalışan uygulamalar sınırlı görevleri gerçekleştirebilirler.Bu başlıkta işletim sisteminin ön planda ve arka planda olan uygulamaları nasıl yönettiğini,yani “Process lifecycle” hiyerarşisini inceleyeceğiz.

1.Foreground process: Kullanılmakta olan uygulama ön plan işlemi olarak kabul edilir.Ayrıca ön planda çalışan uygulama ile etkileşimde olan diğer süreç-process’ler de ön plan olarak kabul edilir.
Dolayısıyla herhangi bir zamanda birkaç ön plan işlemi vardır.

2.Visible process: Bu ön planda olan bir işlem değil ama yine de ekranda görünenleri etkiliyordur. Örneğin ön plandaki işlem,arkasındaki bir uygulamayı görmenize izin veren bir iletişim kutusu olabilir.

3.Service process: Bu süreç,ekranda görünen herhangi bir uygulamaya bağlı olmayabilir. Ancak arka planda müzik dinlemek veya arka planda veri indirmek gibi işler yürütür.

4.Background process: Arka plan işlemleri kullanıcı tarafından görülmezler.Dolayısıyla cihazı kullanma deneyimi üzerinde hiçbir etkisi yoktur. Bu işlemler duraklatılmış uygulamalar olarak düşünebiliriz. Geri döndüğümüzde bunları kullanmaya anında devam edebilmemiz için bellekte tutulurlar(Hot Start). Ancak cihaz kaynakları tüketimi yapmazlar.

5.Empty process: Herhangi bir uygulama verisi içermez ancak boş olarak kalır. Uygulamaların başlatılmasını hızlandırmak için tutulabilir veya gerektiğinde sistem tarafından öldürülebilir.

Otomatik Yönetim

Android işletim sistemi process’leri otomatik olarak yönetir. Dolayısıyla process yönetimi için bir yönetim aracına,örneğin bir görev sonlandırıcıya ihtiyacımız yok.
Sistem ne zaman ki daha fazla kaynağa ihtiyaç duyarsa,process’leri önem sırasına göre öldürmeye başlar. Bu bağlamda ilk sıraya “Empty Process” ve ikinci sıraya “Background Process”’lerini alır.
Ancak daha fazla kaynağa ihtiyaç duyuluyorsa,örneğin çok daha fazla RAM ihtiyacı duyuluyorsa,bu kez öldürme sırasına “Service Process”’ler de girer. Böylelikle yeterli kaynak elde edene kadar servis işlemlerini durdurmaya devam eder. Bu nedenle arka planda yürütülen serivisler,örnek müzik,dosya indirme vb. gibi işlemlerimiz durabilir.
Ancak process yönetimi gelişigüzel yapılmaz. Bu yönetimsel süreç oldukça akıllıdır ve olabildiğince az uygulama,servis vb. süreçlerinin öldürülmesi ve bu süreçlerin seçilme aşaması,ve yine olabildiğince önemsiz process’ler hedef alınarak başlatılır. Dolayısıyla kullanıcının bu konuda endişelenmesine gerek yoktur.

Process yönetimi ayrıca “Memory Management” konusuyla ilişkilidir. Dolayısıyla bir process’in öldürülmesi için gerekli emir MM tarafından verilmiş olabilir.
Ancak yine de istisnalara her zaman yer var.Örneğin kötü kodlanmış bir uygulama arka planda sürekli çalışmaya ve kaynak tüketmeye devam ediyordur ve CPU,power vb. gibi değerli kaynakları ciddi şekilde tüketiyordur. Bu durumda kullanıcıya bir uyarı gösterilebilir ve kullanıcının tercihi neticesine bağlı olarak ilgili process sonlandırılır.
Elbette her kullanıcının bildiği arka plan uygulama listesinde kullanıcının müdahalesiyle tek tek ve/veya “Temizle” buttonu yardımıyla tüm arka plan uygulamaları öldürülebilir.

Process(Süreçler)

Process manager’da her ne kadar beş(5) grup üzerinden açıklıyor olsak da aslında işletim sistemi bu süreçleri yönetmek için bazı değerleri/sabitleri baz alır. Bunları kısaca sıralarsak:
* public static final int BLUETOOTH_UID
Bluetooth servisi için UID/GID’yi tanımlar.

* public static final int FIRST_APPLICATION_UID
İlk uygulamadan itibaren son uyglama(“LAST_APPLICATION_UID”) arasında bir UID aralığı belirlenir.

* public static final int INVALID_UID
Geçersiz bir UID değeri.

* public static final int LAST_APPLICATION_UID
Son uygulama UID değeri.

* public static final int PHONE_UID
Telefon kodunun altında çalıştığı UID/GID tanımlar.

* public static final int ROOT_UID
Root/Kök UID tanımlar. Bu değer linux sisteminde olduğu gibi android’de de ‘0 (0x00000000)’ dır.

* public static final int SHELL_UID
Kullanıcı shell için UID/GID’yi tanımlar.

* public static final int SIGNAL_KILL

* public static final int SIGNAL_QUIT

* public static final int SIGNAL_USR1

* public static final int SYSTEM_UID
Sistem kodunun altında çalıştığı UID/GID’yi tanımlar.

* public static final int THREAD_PRIORITY_AUDIO
Standart “Ses” thread öncelik değeri. Uygulamalar normalde bu öncelik değerine geçiş yapmaz. Ancak öncelikleri değiştirmek için “setThreadPriority(int)” ve “setThreadPriority(int,int)” fonksiyonlarını kullanırız.

* public static final int THREAD_PRIORITY_BACKGROUND
Standart “background” thread öncelik değeri. Bu thread öncelik değeri yukarıda değindiğimiz “Background process”’i temsil eder.
Dolayısıyla bu thread normalden daha düşük bir önceliğe sahiptir.

* public static final int THREAD_PRIORITY_DEFAULT
Uygulama iş parçacıklarının standart önceliğini temsil eder.

* public static final int THREAD_PRIORITY_DISPLAY
Kullanıcı arayüzü(UI) güncellenmesinde yer alan sistem görüntüleme iş parçacıklarının standart önceliğini temsil eder.
Uygulamalar normalde bu önceliğe geçemez.Ancak “
setThreadPriority(int)” ve “setThreadPriority(int,int)” ile bu değiştirilebilir.

* public static final int THREAD_PRIORITY_FOREGROUND
Mevcut olarak kullanıcının etkileşimde olduğu kullanıcı arabirimini çalıştıran iş parçacıklarının standart önceliğini temsil eder.
Uygulamalar normalde bu öncelik seviyesine geçemez. Dolayısıyla kullanıcı arayüzü hareket ettikçe sistem tarafından otomatik olarak bu seviyeler ayarlanır.

* public static final int THREAD_PRIORITY_LESS_FAVORABLE
Bir önceliği daha az faydalı/avantajlı hale getirmek için minimum artış değeri.

*public static final int THREAD_PRIORITY_MORE_FAVORABLE
Bir önceliği daha çok faydalı/avantajlı hale getirmek için minimum artış değeri.

* public static final int THREAD_PRIORITY_LOWEST
Mevcut en düşük iş parçacığı önceliği.

* public static final int THREAD_PRIORITY_URGENT_AUDIO
Ses iş parçacıklarına ait en önemli-acil-ivedi öncelik.

* public static final int THREAD_PRIORITY_URGENT_DISPLAY
Görüntüleme iş parçacıklarına ait en önemli-acil-ivedi öncelik.

* public static final int THREAD_PRIORITY_VIDEO
Video iş parçacıklarının standart önceliği.

* public static final int WIFI_UID
Wifi(wificond,supplicant,hostpad,HAL,…) işlemleri için UID/GID’yi tanımlar.

Android OS kaynak kodları içinde bu tanımlamalar aşağıdadır:
Platform/threads.h
system/core/thread_defs.h

Aşağıdaki örnekte bir thread öncelik(priority) ayarlaması yapılmaktadır.
Örnek:

Problem Tespit ve Teşhis

Uygulama başlangıç zamanı performansını doğru şekilde teşhis etmek için,uygulamanın başlamasının ne kadar sürdüğünü gösteren metriksleri takip edebiliriz. Android,uygulamamızın bir sorunu olduğunu bize bildirmek ve sorunu teşhis etmek için çeşitli yollar-yaklaşımlar sunar.
“Vitals” bu araçlardan biridir.

Anroid Vitals

Android,”Cold Start” ve “Warm Start” uygulama başlatmalarını optimize etmek için ilk görüntülemeye ve tam görüntülemeye kadar geçen süre ölçümlerini kullanır.
Vitals,uygulamamızın başlatma süreleri aşırı yüksek olduğunda Play Console aracılığıyla bizi uyararak performansı iyileştirmeye yardımcı olur.Bu araç aşağıdaki durumlarda uygulamamızın başlangıç sürelerinin aşırı yüksek olduğunu düşünür:
* “Cold Start” 5 saniye veya daha uzun sürerse.
* “Warn Start” 2 saniye veya daha uzun sürerse.
* “Hot Start” 1.5 saniye veya daha uzun sürerse.

TTID(Time to initial Display),ilk görüntüleme zamanı metriği,işlem başlatma(Cold start),Activity oluşturma(Cold/Wam start) ve ilk kareyi ekranda görüntüleme dahil olmak üzere bir uygulamanın ilk çerçevesinni üretmesi-çizmesi için geçen süreyi ölçer.
TTID,Android 4.4 ve sonraki sürümlerde logcat’da “Display” etiketi içieren bir log çıktısı içerir.Bu satırda yer alan değer,sürecin başlatılması ile ilgili ve Activity’in ekranda çizilmesinin bitirilmesi arasında geçen süreyi temsil eder. Geçen süre aşağıdaki olaylar dizisini kapsar:
*Launch the process(İşlemi başlat).
*Initialize the objects(Nesneleri başlat).
*Create and initialize the activity(Activity oluştur ve başlat).
*Inflate the layout.
*Draw Application(Uygulamayı çiz. ilk kez çiziliyor).

Log çıktısı aşağıdaki örneğe benzerdir:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

Komut satırından veya bir terminalde logcat çıktısını izliyor olabiliriz ve bu çıktılar içinde geçen süreyi,yani yukarıdaki satırı bulmak kolaydır.
Android Studio’da geçen süreyi bulmak için logcat arayüzünde filtreleri devre dışı bırakmak gerekir. Çünkü bu satır uygulamaya ait bir log çıktısı değil,doğrudan işletim sistemi tarafından sağlanıyor. Aşağıdaki görselde bu işlem verilmiştir.

Bazen logcat çıktısındaki “Displayed” satırı,toplam süre için ek bir alan içerir. Örnek:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)

Bu durumda ilk ölçüm sadece ilk çizilen Activity içindir. "total" süre ölçümü,uygulama process'i başlangıcında başlar ve ilk olarak başlatılan ancak ekranda gösterilmeyen başka bir Activity içerebilir. Dolayısıyla "total" süre ölçümü,yalnızca tek Activity ile toplam başlatma süreleri arasında bir fark olduğunda gösterilir.

Ayrıca uygulamamızı "ADB Shell Activity Manager" komutuyla çalıştırarak ilk görüntüleme süresini ölçebiliriz. Örnek:

adb [-d|-e|-s <serialNumber>] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN

Komutu çalıştırdıktan sonra logcat çıktısında “Displayed” daha önce olduğu gibi görünür.Ayrıca aşağıdaki çıktı terminal penceresinde gösterilir:

Starting: Intent
Activity: com.example.app/.MainActivity
ThisTime: 2044
TotalTime: 2044
WaitTime: 2054
Complete

Komutta yer alan “-c” ve “-a” parametreleri isteğe bağlıdır ve “<category>”,”<action>” belirtmemize izin verir.

TTFD(Time to full Display)
TTFD metriği,ilk frame(çerçeve)’den sonra eş zamansız olarak yüklenen içerik de dahil olmak üzere,uygulamanın ilk çerçevesini tam içerikle oluşturmak için geçen süreyi ölçer.
Uygulamanın başlatılması ile tüm kaynakların tam olarak görüntülenmesi arasında geçen süreyi ölçmek ve hiyerarşileri görüntülemek için “reportFullyDrawn()” fonksiyonu kullanılabilir.
Bu fonksiyonu kullanmak bir uygulamanın “lazy loading” yaptığı durumlarda kullanışlı olabilir.

NOT:”lazy loading”,uygulamaya ait görseller ekrana çizilmiştir ancak örneğin network vasıtasıyla alınan bir text ve/veya resim içeriği henüz yüklenmemiştir.
Bu durum “lazy loading”’a bir örnektir. Yani aslında ImageView çizimi yapılmıştır ancak network üzerinden uzak sunucudan çekilen görsel bu View’a henüz ulaşamamıştır.

Lazy loading’de bir uygulama penceresinin çizimi engellenmez. Bunun yerine kaynakları eşzamansız olarak yükler ve görünüm hiyerarşisini günceller. Bu durumda “lazy loading”’in bittiğini sisteme bildirmek için “reportFullyDrawn()” fonksiyonunu manuel olarak çağırabiliriz. Bu fonksiyon kullanıldığında,logcat’in görüntülediği değer,uygulama nesnesnin oluşturulmasından “reportFullyDrawn()” çağrılmasına kadar geçen süredir. Örnek çıktı:

system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms

Dolayısıyla bu verilere dayanarak uygulama başlangıç sürecindeki darboğazları(bottlenecks) belirleyebiliriz.
Darboğazları görmenin en iyi yolu ise “Android Studio CPU Profiler” aracıdır. Bu araç hakkında bilgiye bu linkten ulaşabilirsiniz.
Ayrıca uygulamalarımızın ve Activitylerimizin “onCreate()” fonksiyonları içindeki satır içi izleme yoluyla olası darboğazları hakkında fikir edinebiliriz.
Satır içi izleme hakkında bilgi edinmek için “Trace" fonksiyonlarına ve “system tracing" kullanabiliriz.

Sonuç

Geliştirdiğimiz uygulamanın başlangıç performansı çeşitli sebeplerle kötü-düşük olabilir. Bunlar “Bad Code”,”Architectural”,”Lazy Loading” ve
Heavy Initialization” kaynaklı olabilir.

NOT: “heavy initialization”’a sebep, uygulama nesneleri ve/veya Activity’ler başlatılırken,bunlara ait ancak gerekliliği acil olmayan alt class’lar vb. ile uğraş sonucu ortaya çıkar.
Aslında bu da “Architectural” kaynaklı bir sorundur ancak ayrı olarak sınıflandırmak faydalı olacaktır. Dolayısıyla bir Activity’i kullanıcıya bir an önce göstermek için çabalamak yerine,o activity’a ait ancak kullanıcı etkileşimi için aciliyeti olmayan alt işlerle uğraşmak “heavy init”’a örnektir.

Bu durumları ve darboğazları,problemleri tespit etmek için çeşitli kavramlar(Method Tracing,Inline Tracing) konuştuk.
Ve aynı zamanda “Architectural” anlamda çeşitli kütüphaneler(Hilt,App Startup vb.) kullanılabilir.

Lazy loading ve Heavy Init sebepleri için ise öncelikli olarak iyileştirmemiz gereken konular:
* Büyük ve kompleks layoutlar.
* Disk ve/veya Network bağlantılı görseller ve kaynaklar.
* Bitmap yüklenme ve decode olma aşamaları.
* VectorDrawable objeleri.
* Activity alt sistemlerinin başlatılması-ilişkileri.
* Uygulama ve Activity yüklenme aşamalarında GC süreçleri
*… gibi konular incelenebilir ve iyileştirmeler yapılabilir. Yukarıda belirttiğim maddelerin her biri için devasa kaynaklar üretmek icap eder. Fırsat buldukça madde/konu bazlı içerikler üretmeye devam edeceğim.

Son olarak ise “Splash Screen” yöntemini belirterek yazıyı sonlandırıyorum. Bu yöntem aslında tüm bu konuştuğumuz problemler sonucu oluşan Startup Time durumunda kullanıcıya boş bir ekran göstermek yerine,ürün-şirket ile ilgili veya benzer bir görsel(video,animasyon vb.) sunmaktır.
https://developer.android.com/reference/android/window/SplashScreen
https://developer.android.com/develop/ui/views/launch/splash-screen/migrate

Startup Time ve Performans odaklı daha detaylı noktasal bazda yazılarım olacak. Ancak bu makalede genel olarak bir android uygulamasının başlatılma süreci,bu süreci etkileyen faktörler ve genel anlamda tespit yöntemlerini inceledik.

KAYNAKLAR:
https://developer.android.com/
https://developer.android.com/topic/performance/vitals/launch-time

-end of

--

--

Kerim Fırat

Senior C++,Android Platform(AOSP,AAOS) Developer,Performance Architect,Open Source Contributor | Turkey Java User Group Vice Chairman | Author