İleti dizileri sayesinde daha iyi performans

Android'deki ileti dizilerini doğru şekilde kullanmak, uygulamanızın performansını artırmanıza yardımcı olabilir. Bu sayfada iş parçacıklarıyla çalışmanın çeşitli yönleri ele alınmaktadır: kullanıcı arayüzü veya ana iş parçacığı ile çalışma; uygulama yaşam döngüsü ile iş parçacığı önceliği arasındaki ilişki ve iş parçacığı karmaşıklığını yönetmek için platformun sağladığı yöntemler. Bu alanların her birinde bu sayfada olası tehlikeler ve bunlardan kaçınmaya yönelik stratejiler açıklanmaktadır.

Ana iş parçacığı

Kullanıcı uygulamanızı başlattığında Android, bir yürütme iş parçacığıyla birlikte yeni bir Linux işlemi oluşturur. Kullanıcı arayüzü iş parçacığı olarak da bilinen bu ana iş parçacığı, ekranda olan her şeyden sorumludur. Nasıl çalıştığını anlamak, uygulamanızı mümkün olan en iyi performans için ana iş parçacığını kullanacak şekilde tasarlamanıza yardımcı olabilir.

Dahililer

Ana iş parçacığı çok basit bir tasarıma sahiptir: Tek işi, uygulaması sonlandırılana kadar iş parçacığı güvenli bir iş kuyruğundan iş bloklarını alıp yürütmektir. Çerçeve, bu blokların bir kısmını farklı yerlerden üretiyor. Bu yerler arasında yaşam döngüsü bilgileriyle ilişkili geri çağırmalar, giriş gibi kullanıcı etkinlikleri veya diğer uygulama ve süreçlerden gelen etkinlikler yer alır. Ayrıca uygulama, çerçeveyi kullanmadan blokları açık bir şekilde kendi başına sıraya koyabilir.

Uygulamanızın yürüttüğü neredeyse tüm kod blokları; giriş, düzen şişirme veya beraberlik gibi bir etkinlik geri çağırmasına bağlıdır. Bir öğe bir etkinliği tetiklediğinde, etkinliğin gerçekleştiği ileti dizisi etkinliği kendi dışına ve ana ileti dizisinin mesaj sırasına aktarır. Böylece ana iş parçacığı etkinliği sunabilir.

Bir animasyon veya ekran güncellemesi gerçekleşirken sistem, saniyede 60 kare hızında sorunsuz şekilde görüntülenebilmesi için yaklaşık 16 ms'de bir iş bloğunu (ekranın çizilmesinden sorumludur) yürütmeye çalışır. Sistemin bu hedefe ulaşabilmesi için kullanıcı arayüzü/görünüm hiyerarşisinin ana iş parçacığında güncellenmesi gerekir. Ancak ana iş parçacığının mesajlaşma sırası, ana iş parçacığının güncellemeyi yeterince hızlı bir şekilde tamamlayamayacağı kadar çok veya çok uzun görevler içerdiğinde, uygulama bu çalışmayı bir çalışan ileti dizisine taşımalıdır. Ana iş parçacığı çalışma bloklarını 16 ms içinde yürütmeyi bitiremezse kullanıcı kilitlenme, gecikme veya girişe karşı kullanıcı arayüzü duyarlılığının olmadığını gözlemleyebilir. Ana iş parçacığı yaklaşık beş saniye boyunca engellerse sistem, Uygulama Yanıt Vermiyor (ANR) iletişim kutusunu görüntüleyerek kullanıcının uygulamayı doğrudan kapatmasına olanak tanır.

Çok sayıda veya uzun görevi ana iş parçacığından, sorunsuz oluşturma yapılmasını ve kullanıcı girişine hızlı yanıt verilmesini engellemeyecek şekilde taşımak, uygulamanızda iş parçacığı kullanmaya başlamanın en önemli nedenidir.

İş dizileri ve kullanıcı arayüzü nesne referansları

Tasarım gereği Android View nesneleri iş parçacığına uygun değildir. Bir uygulamanın, tamamen ana iş parçacığında kullanıcı arayüzü nesneleri oluşturması, kullanması ve kaldırması beklenir. Ana iş parçacığı dışında bir iş parçacığında bir kullanıcı arayüzü nesnesini değiştirmeye veya referans almaya çalışırsanız sonuçta istisnalar, sessiz hatalar, kilitlenmeler ve tanımlanmamış diğer yanlış davranışlar ortaya çıkabilir.

Referanslarla ilgili sorunlar iki farklı kategoriye ayrılır: açık referanslar ve örtülü referanslar.

Uygunsuz referanslar

Ana olmayan iş parçacıklarındaki birçok görevin nihai hedefi, kullanıcı arayüzü nesnelerini güncellemektir. Bununla birlikte, bu iş parçacıklarından biri görünüm hiyerarşisindeki bir nesneye erişirse uygulama kararsızlığı ortaya çıkabilir: Bir çalışan iş parçacığı, başka bir iş parçacığının nesneye referansta bulunduğu anda bu nesnenin özelliklerini değiştirirse sonuçlar tanımsız olur.

Örneğin, bir çalışan iş parçacığında kullanıcı arayüzü nesnesine doğrudan referans bulunduran bir uygulama düşünün. Çalışan iş parçacığındaki nesne bir View öğesine referans içeriyor olabilir ancak iş tamamlanmadan önce View, görünüm hiyerarşisinden kaldırılır. Bu iki işlem aynı anda gerçekleştiğinde, referans View nesnesini bellekte tutar ve özelliklerini ayarlar. Ancak, kullanıcı bu nesneyi hiçbir zaman görmez ve uygulama, referansı silindikten sonra nesneyi siler.

Başka bir örnekte, View nesneleri bunların sahibi olan etkinliğe referanslar içerir. Bu etkinlik imha edilirse ancak doğrudan veya dolaylı olarak ona referans veren iş parçacıklı bir iş bloğu kalırsa çöp toplayıcı, söz konusu iş blokunun yürütmesi bitene kadar etkinliği toplamaz.

Bu senaryo, ekran döndürme gibi bazı etkinlik yaşam döngüsü olaylarının gerçekleşmesi sırasında iş parçacıklı işin aktarımda olabileceği durumlarda soruna neden olabilir. Sistem, hazırlık çalışması tamamlanana kadar çöp toplama işlemi yapamaz. Sonuç olarak, çöp toplama işlemi gerçekleşene kadar bellekte iki Activity nesnesi bulunabilir.

Bu gibi senaryolarda, uygulamanızın mesaj dizili iş görevlerindeki kullanıcı arayüzü nesnelerine açık referanslar eklememesini öneririz. Bu tür referanslardan kaçınmak bu tür bellek sızıntılarını önlemenize ve ileti dizisi çakışmasından kaçınmanıza yardımcı olur.

Her durumda, uygulamanız yalnızca ana iş parçacığındaki kullanıcı arayüzü nesnelerini güncellemelidir. Bu nedenle, birden fazla iş parçacığının çalışmayı ana iş parçacığına geri iletmesine izin veren bir pazarlık politikası oluşturmanız gerekir. Bu politika, gerçek kullanıcı arayüzü nesnesini güncelleme işiyle en üstteki etkinliği veya parçayı görevlendirir.

Örtülü referanslar

İş parçacıklı nesnelerde yaygın olarak görülen bir kod tasarımı hatası, aşağıdaki kod snippet'inde görülebilir:

Kotlin

class MainActivity : Activity() {
    // ...
    inner class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
        override fun doInBackground(vararg params: Unit): String {...}
        override fun onPostExecute(result: String) {...}
    }
}

Java

public class MainActivity extends Activity {
  // ...
  public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

Bu snippet'teki hata, kodun iş parçacığı (MyAsyncTask) öğesini bir etkinliğin statik olmayan iç sınıfı (veya Kotlin'de bir iç sınıf) olarak tanımlamasıdır. Bu bildirim, çevreleyen Activity örneğine dolaylı bir referans oluşturur. Sonuç olarak nesne, iş parçacıklı çalışma tamamlanana kadar etkinliğe bir referans içerir ve bu da, başvurulan etkinliğin yok edilmesinde gecikmeye neden olur. Bu gecikme de bellek üzerinde daha fazla baskı yaratır.

Bu sorunun doğrudan bir çözümü, aşırı yüklenen sınıf örneklerinizi statik sınıflar olarak veya kendi dosyalarında tanımlayarak dolaylı referansı kaldırmak olacaktır.

Başka bir çözüm de arka plan görevlerinin her zaman onDestroy gibi uygun Activity yaşam döngüsü geri çağırmasında iptal edilip temizlenmesidir. Ancak bu yaklaşım yorucu ve hataya yatkın olabilir. Genel bir kural olarak, karmaşık, kullanıcı arayüzü olmayan mantığı doğrudan etkinliklere yerleştirmemelisiniz. Ayrıca, AsyncTask artık kullanımdan kaldırılmıştır ve yeni kodda kullanılması önerilmez. Kullanabileceğiniz eşzamanlılık temel öğeleri hakkında daha fazla bilgi için Android'de Threading bölümüne bakın.

İleti dizileri ve uygulama etkinliği yaşam döngüleri

Uygulama yaşam döngüsü, iş parçacığı işlemenin uygulamanızdaki işleyiş şeklini etkileyebilir. Bir etkinlik kaldırıldıktan sonra bir iş parçacığının devam etmesi veya kalmaması gerektiğine karar vermeniz gerekebilir. İş parçacığı önceliği ile bir etkinliğin ön planda mı arka planda mı çalıştığı arasındaki ilişkiyi de göz önünde bulundurmanız gerekir.

Devam eden ileti dizileri

İleti dizileri, onları oluşturan etkinliklerin ömrü boyunca varlığını sürdürür. İş parçacıkları, etkinliklerin oluşturulması veya kaldırılmasından bağımsız olarak kesintisiz bir şekilde çalışmaya devam eder. Bununla birlikte, başka etkin uygulama bileşeni kalmadığında uygulama işlemiyle birlikte sonlandırılırlar. Bazı durumlarda bu kalıcılık istenendir.

Bir etkinliğin iş parçacığı şeklinde bir iş bloğu oluşturduğu ve bir çalışan iş parçacığı blokları yürütmeden önce yok edildiği bir durumu düşünün. Uygulamanın uçan bloklarla ne yapması gerekiyor?

Engellemeler artık mevcut olmayan bir kullanıcı arayüzünü güncelleyecekse çalışmanın devam etmesi için bir neden yoktur. Örneğin, iş bir veritabanından kullanıcı bilgilerini yükleyip ardından görünümleri güncellemekse iş parçacığı artık gerekli değildir.

Buna karşılık iş paketleri, kullanıcı arayüzüyle tamamen ilgili olmayan bazı avantajlara sahip olabilir. Bu durumda, ileti dizisini devam ettirmeniz gerekir. Örneğin, paketler bir görüntüyü indirmeyi, diskte önbelleğe almayı ve ilişkili View nesnesini güncellemeyi bekliyor olabilir. Nesne artık mevcut olmasa da kullanıcının yok edilen etkinliğe geri dönme olasılığına karşı görüntüyü indirme ve önbelleğe alma işlemleri yararlı olabilir.

Tüm iş parçacığı nesneleri için yaşam döngüsü yanıtlarını manuel olarak yönetmek son derece karmaşık olabilir. Bunları doğru bir şekilde yönetmezseniz uygulamanızda bellek çakışması ve performans sorunları yaşanabilir. ViewModel'i LiveData ile birleştirmek, verileri yüklemenize ve yaşam döngüsü konusunda endişelenmenize gerek kalmadan değişiklik olduğunda bildirim almanıza olanak tanır. ViewModel nesne, bu soruna yönelik bir çözümdür. ViewModel'ler, yapılandırma değişiklikleri boyunca korunur ve bu sayede görüntüleme verilerinizi kolayca koruyabilirsiniz. ViewModels hakkında daha fazla bilgi için ViewModel kılavuzuna, LiveData hakkında daha fazla bilgi için LiveData kılavuzuna bakın. Uygulama mimarisi hakkında da daha fazla bilgi edinmek için Uygulama Mimarisi Kılavuzu'nu okuyun.

İş parçacığı önceliği

Süreçler ve Uygulama Yaşam Döngüsü bölümünde açıklandığı gibi, uygulamanızın iş parçacıklarının aldığı öncelik kısmen, uygulamanın uygulama yaşam döngüsünde nereye geldiğine bağlıdır. Uygulamanızda iş parçacıklarını oluşturup yönetirken, doğru iş parçacıklarının doğru zamanlarda doğru öncelikleri alabilmesi için önceliklerini ayarlamanız önemlidir. Çok yüksek bir değere ayarlanırsa iş parçanız, kullanıcı arayüzü iş parçacığını ve RenderThread'i keserek uygulamanızın kare bırakmasına neden olabilir. Çok düşük bir değere ayarlanırsa eşzamansız görevlerinizi (resim yükleme gibi) olması gerekenden daha yavaş hale getirebilirsiniz.

Her ileti dizisi oluşturduğunuzda setThreadPriority() numaralı telefonu çağırmalısınız. Sistemin iş parçacığı planlayıcısı, yüksek önceliklere sahip iş parçacıklarına öncelik verir ve bu öncelikler ile nihayetinde tüm işin tamamlanmasını sağlama ihtiyacı arasında denge kurar. Genel olarak, ön plan grubundaki iş parçacıkları cihazdan toplam yürütme süresinin yaklaşık%95'ini alırken arka plan grubu yaklaşık %5'ini alır.

Sistem ayrıca Process sınıfını kullanarak her iş parçacığına kendi öncelik değerini atar.

Varsayılan olarak sistem bir iş parçacığının önceliğini, ortaya çıkan iş parçacığıyla aynı önceliğe ve grup üyeliklerine ayarlar. Ancak uygulamanız, setThreadPriority() kullanarak iş parçacığı önceliğini açıkça ayarlayabilir.

Process sınıfı, uygulamanızın iş parçacığı önceliklerini ayarlamak için kullanabileceği bir sabit değerler sağlayarak öncelik değerleri atama işleminin karmaşıklığını azaltmaya yardımcı olur. Örneğin, THREAD_PRIORITY_DEFAULT, bir iş parçacığının varsayılan değerini temsil eder. Uygulamanız, acil işler yürüten iş parçacıkları için iş parçacığının önceliğini THREAD_PRIORITY_BACKGROUND olarak ayarlamalıdır.

Uygulamanız göreli öncelikleri ayarlamak için THREAD_PRIORITY_LESS_FAVORABLE ve THREAD_PRIORITY_MORE_FAVORABLE sabit değerlerini artımlı olarak kullanabilir. İş parçacığı önceliklerinin listesi için Process sınıfındaki THREAD_PRIORITY sabitlerine bakın.

İş parçacıklarını yönetme hakkında daha fazla bilgi için Thread ve Process sınıflarıyla ilgili referans belgelerine bakın.

İleti dizileri için yardımcı sınıfları

Birincil dil olarak Kotlin kullanan geliştiriciler için eşleşenlerin kullanılmasını öneririz. Eşzamanlılar; geri çağırmalar olmadan eşzamansız kod yazmanın yanı sıra kapsam oluşturma, iptal ve hata işleme için yapılandırılmış eşzamanlılık dahil olmak üzere çok sayıda avantaj sağlar.

Çerçeve, iş parçacığı oluşturmayı kolaylaştırmak için Thread, Runnable ve Executors sınıflarının yanı sıra HandlerThread gibi ek sınıfları ve temel öğeleri de sağlar. Daha fazla bilgi için lütfen Android'de Threading sayfasına bakın.

HandlerThread sınıfı

İşleyici iş parçacığı, bir sıradaki işleri alıp üzerinde çalışan uzun süreli bir iş parçacığıdır.

Camera nesnenizden önizleme kareleri almayla ilgili yaygın bir sorun olduğunu düşünün. Kamera önizleme çerçevelerine kaydolduğunuzda, bunları çağrıldığı etkinlik iş parçacığında çağrılan onPreviewFrame() geri çağırmasıyla alırsınız. Bu geri çağırma, kullanıcı arayüzü iş parçacığında çağrılmışsa büyük piksel dizileriyle ilgilenme görevi, oluşturma ve etkinlik işleme çalışmalarını engellemek olur.

Bu örnekte, uygulamanız Camera.open() komutu için işleyici iş parçacığındaki bir çalışma blokuna yetki verdiğinde, ilişkilendirilen onPreviewFrame() geri çağırması kullanıcı arayüzü iş parçacığı yerine işleyici iş parçacığına ulaşır. Bu nedenle, pikseller üzerinde uzun süreli çalışma yapacaksanız bu sizin için daha iyi bir çözüm olabilir.

Uygulamanız HandlerThread kullanarak bir iş parçacığı oluşturduğunda, iş parçacığının yaptığı iş türüne göre önceliğini ayarlamayı unutmayın. CPU'ların paralel olarak yalnızca az sayıda iş parçacığını işleyebileceğini unutmayın. Önceliği ayarlamak, diğer tüm iş parçacıkları dikkat çekmek için mücadele ederken sistemin bu işi programlaması için doğru yolları bilmesine yardımcı olur.

ThreadPoolExecutor sınıfı

Yüksek oranda paralel ve dağıtılmış görevlere indirilebilecek belirli iş türleri vardır. Örneğin bu tür bir görev, 8 megapiksellik bir resmin her 8x8 bloğu için filtre hesaplamaktır. Bunun oluşturduğu iş paketlerinin büyük hacmi nedeniyle HandlerThread, kullanılması uygun bir sınıf değildir.

ThreadPoolExecutor, bu işlemi kolaylaştırmak için bir yardımcı sınıftır. Bu sınıf, bir iş parçacığı grubunun oluşturulmasını yönetir, önceliklerini belirler ve işin bu ileti dizileri arasında nasıl dağıtıldığını yönetir. İş yükü arttıkça veya azaldıkça sınıf, iş yüküne uyum sağlamak için daha fazla iş parçacığını hızlandırır veya yok eder.

Bu sınıf, uygulamanızın optimum sayıda iş parçacığı oluşturmasına da yardımcı olur. Uygulama bir ThreadPoolExecutor nesnesi oluşturduğunda minimum ve maksimum iş parçacığı sayısını ayarlar. ThreadPoolExecutor için verilen iş yükü arttıkça sınıf, başlatılmış minimum ve maksimum iş parçacığı sayısını hesaba katar ve bekleyen iş miktarını göz önünde bulundurur. ThreadPoolExecutor, bu faktörlere bağlı olarak herhangi bir zamanda kaç iş parçacığının aktif olması gerektiğine karar verir.

Kaç ileti dizisi oluşturmalısınız?

Yazılım düzeyinden bile olsa kodunuz yüzlerce iş parçacığı oluşturabilir. Bu durum, performans sorunlarına neden olabilir. Uygulamanız arka plan hizmetleri, oluşturucu, ses motoru, ağ iletişimi ve daha fazlasıyla sınırlı CPU kaynaklarını paylaşıyor. CPU'lar aslında yalnızca az sayıda iş parçacığını paralel olarak işleyebilir. Yukarıdaki her şey öncelik ve planlama sorununa bağlıdır. Bu nedenle, yalnızca iş yükünüz için ihtiyaç duyduğunuz kadar iş parçacığı oluşturmanız önemlidir.

Pratikte bunun nedeni olan birçok değişken vardır. Ancak bir değer seçmek (başlangıç için 4 gibi) ve bunu Systrace ile test etmek diğer yöntemler kadar etkili bir stratejidir. Herhangi bir sorunla karşılaşmadan kullanabileceğiniz minimum iş parçacığı sayısını öğrenmek için deneme yanılma yöntemini kullanabilirsiniz.

Kaç iş parçacığı olacağına karar verirken dikkate alınması gereken bir başka nokta da iş parçacıklarının ücretsiz olmamasıdır; yani bellekte yer kaplarlar. Her iş parçacığının minimum 64.000 bellek maliyeti vardır. Bu, özellikle çağrı yığınlarının önemli ölçüde büyüdüğü durumlarda, bir cihazda yüklü olan çok sayıda uygulamada hızlı bir şekilde ortaya çıkar.

Birçok sistem işlemi ve üçüncü taraf kitaplığı genellikle kendi iş parçacık havuzlarını başlatır. Uygulamanız mevcut bir iş parçacığı havuzunu yeniden kullanabiliyorsa bu yeniden kullanım, bellek ve işleme kaynakları arasındaki çakışmayı azaltarak performansa yardımcı olabilir.