Kotlin eş yordamlarıyla uygulama performansını iyileştirme

Kotlin eş yordamları, ağ çağrıları veya disk işlemleri gibi uzun süren görevleri yönetirken uygulamanızın yanıt vermesini sağlayan temiz ve basitleştirilmiş zaman uyumsuz kod yazmanıza olanak tanır.

Bu konuda, Android'deki eş yordamlar ayrıntılı olarak ele alınmaktadır. Eş yordamlar hakkında bilginiz yoksa bu konuyu okumadan önce Android'de Kotlin eş yordamları başlıklı makaleyi okuduğunuzdan emin olun.

Uzun süreli görevleri yönetme

Coroutines, uzun süren görevleri yönetmek için iki işlem ekleyerek normal işlevler üzerine kurulur. invoke (veya call) ve return'ye ek olarak, eş yordamlar suspend ve resume'yi de ekler:

  • suspend, geçerli eş yordamın yürütülmesini duraklatır ve tüm yerel değişkenleri kaydeder.
  • resume, askıya alınmış bir eş yordamın yürütülmesine askıya alındığı yerden devam eder.

suspend işlevlerini yalnızca diğer suspend işlevlerinden veya yeni bir eşzamanlı rutin başlatmak için launch gibi bir eşzamanlı rutin oluşturucu kullanarak çağırabilirsiniz.

Aşağıdaki örnekte, varsayımsal olarak uzun süren bir görev için basit bir eş yordam uygulaması gösterilmektedir:

suspend fun fetchDocs() { // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result) // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) {
    /* ... */
}

Bu örnekte, get() ana iş parçacığında çalışmaya devam eder ancak ağ isteğini başlatmadan önce eş yordamı askıya alır. Ağ isteği tamamlandığında get, ana ileti dizisini bilgilendirmek için geri çağırma işlevi kullanmak yerine askıya alınmış eş yordamı devam ettirir.

Kotlin, hangi işlevin yerel değişkenlerle birlikte çalıştığını yönetmek için yığın çerçevesi kullanır. Bir eş yordam askıya alındığında mevcut yığın çerçevesi kopyalanır ve daha sonra kullanılmak üzere kaydedilir. Devam ettirildiğinde yığın çerçevesi kaydedildiği yerden geri kopyalanır ve işlev tekrar çalışmaya başlar. Kod, sıradan bir sıralı engelleme isteği gibi görünse de eş yordam, ağ isteğinin ana iş parçacığını engellemesini önler.

Ana iş parçacığında güvenli olmak için coroutine'leri kullanın

Kotlin eş yordamları, eş yordam yürütme için hangi iş parçacıklarının kullanılacağını belirlemek üzere göndericileri kullanır. Kodu ana iş parçacığı dışında çalıştırmak için Kotlin coroutines'a Default veya IO dağıtıcısında çalışmasını söyleyebilirsiniz. Kotlin'de tüm coroutine'ler ana iş parçacığında çalışırken bile bir dağıtıcıda çalışmalıdır. Coroutine'ler kendilerini askıya alabilir ve bunları devam ettirmekten gönderici sorumludur.

Kotlin, eş yordamların nerede çalışacağını belirtmek için kullanabileceğiniz üç dağıtıcı sağlar:

  • Dispatchers.Main: Ana Android iş parçacığında bir eş yordam çalıştırmak için bu dağıtıcıyı kullanın. Bu özellik yalnızca kullanıcı arayüzüyle etkileşim kurmak ve hızlı işlemler yapmak için kullanılmalıdır. suspend işlevlerini çağırma, Android kullanıcı arayüzü çerçevesi işlemlerini çalıştırma ve LiveData nesnelerini güncelleme buna örnek olarak verilebilir.
  • Dispatchers.IO: Bu dağıtıcı, ana iş parçacığı dışında disk veya ağ G/Ç'si gerçekleştirmek için optimize edilmiştir. Örnek olarak Room bileşeninin kullanılması, dosyalardan okuma veya dosyalara yazma işlemleri ve herhangi bir ağ işleminin çalıştırılması verilebilir.
  • Dispatchers.Default: Bu dağıtıcı, CPU yoğunluklu işleri ana iş parçacığı dışında gerçekleştirecek şekilde optimize edilmiştir. Liste sıralama ve JSON ayrıştırma gibi kullanım alanları örnek verilebilir.

Önceki örneğe devam edersek göndericileri kullanarak get işlevini yeniden tanımlayabilirsiniz. get gövdesinde, withContext(Dispatchers.IO) işlevini çağırarak giriş/çıkış iş parçacığı havuzunda çalışan bir blok oluşturun. Bu bloğun içine yerleştirdiğiniz tüm kodlar her zaman IO dağıtıcı aracılığıyla yürütülür. withContext, askıya alma işlevi olduğundan get işlevi de askıya alma işlevidir.

suspend fun fetchDocs() { // Dispatchers.Main
    val result = get("developer.android.com") // Dispatchers.Main
    show(result) // Dispatchers.Main
}

suspend fun get(url: String) = // Dispatchers.Main
    withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
        /* perform network IO here */ // Dispatchers.IO (main-safety block)
    } // Dispatchers.Main

Eş yordamlarla, iş parçacıklarını ayrıntılı kontrolle gönderebilirsiniz. withContext(), geri çağırmaları kullanmadan herhangi bir kod satırının iş parçacığı havuzunu kontrol etmenize olanak tanıdığından, bunu veritabanından okuma veya ağ isteği gerçekleştirme gibi çok küçük işlevlere uygulayabilirsiniz. Her işlevin main-safe olduğundan emin olmak için withContext() kullanmak iyi bir uygulamadır. Bu, işlevi ana iş parçacığından çağırabileceğiniz anlamına gelir. Bu sayede, arayanın işlevi yürütmek için hangi iş parçacığının kullanılması gerektiğini düşünmesi gerekmez.

Önceki örnekte fetchDocs() ana iş parçacığında yürütülür ancak arka planda ağ isteği gerçekleştiren get'u güvenli bir şekilde çağırabilir. Coroutine'ler suspend ve resume'yi desteklediğinden, withContext bloğu tamamlanır tamamlanmaz ana iş parçacığındaki coroutine, get sonucuyla devam ettirilir.

withContext() işlevinin performansı

withContext(), eşdeğer geri çağırmaya dayalı bir uygulamaya kıyasla ek yük getirmez. Ayrıca, bazı durumlarda withContext() aramalarını eşdeğer bir geri arama tabanlı uygulamadan daha iyi şekilde optimize etmek mümkündür. Örneğin, bir işlev ağa on kez çağrı yapıyorsa dış withContext() kullanarak Kotlin'e yalnızca bir kez iş parçacığı değiştirmesini söyleyebilirsiniz. Bu durumda, ağ kitaplığı withContext() öğesini birden fazla kez kullansa da aynı dağıtıcıda kalır ve iş parçacıklarını değiştirmekten kaçınır. Ayrıca Kotlin, mümkün olduğunda iş parçacığı geçişlerini önlemek için Dispatchers.Default ve Dispatchers.IO arasında geçişi optimize eder.

Coroutine başlatma

Eş yordamları iki şekilde başlatabilirsiniz:

  • launch yeni bir eş yordam başlatır ve sonucu arayana döndürmez. "Başlat ve unut" olarak kabul edilen tüm çalışmalar launch kullanılarak başlatılabilir.
  • async yeni bir eş yordam başlatır ve await adlı bir askıya alma işleviyle sonuç döndürmenize olanak tanır.

Normal bir işlev await işlevini çağıramadığından genellikle normal bir işlevden yeni bir launch eş yordam başlatmanız gerekir. async işlevini yalnızca başka bir eş yordamın veya askıya alma işlevinin içindeyken ve paralel ayrıştırma gerçekleştirirken kullanın.

Paralel ayrıştırma

suspend işlevi içinde başlatılan tüm eşzamanlı rutinler, bu işlev döndüğünde durdurulmalıdır. Bu nedenle, döndürmeden önce bu eşzamanlı rutinlerin tamamlanmasını sağlamanız gerekir. Kotlin'deki yapılandırılmış eşzamanlılık ile bir veya daha fazla eşrutini başlatan bir coroutineScope tanımlayabilirsiniz. Ardından, await() (tek bir eş yordam için) veya awaitAll() (birden fazla eş yordam için) kullanarak bu eş yordamların işlevden dönmeden önce tamamlanmasını sağlayabilirsiniz.

Örneğin, iki belgeyi eşzamansız olarak getiren bir coroutineScope tanımlayalım. Her ertelenmiş referansta await() çağrısı yaparak, bir değer döndürülmeden önce her iki async işlemin de tamamlanacağını garanti ederiz:

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

Aşağıdaki örnekte gösterildiği gibi, koleksiyonlarda da awaitAll() simgesini kullanabilirsiniz:

suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf( // fetch two docs at the same time
            async { fetchDoc(1) }, // async returns a result for the first doc
            async { fetchDoc(2) } // async returns a result for the second doc
        )
        deferreds.awaitAll() // use awaitAll to wait for both network requests
    }

fetchTwoDocs(), async ile yeni eşzamanlı rutinler başlatmasına rağmen işlev, döndürmeden önce başlatılan eşzamanlı rutinlerin tamamlanmasını beklemek için awaitAll() kullanır. Ancak awaitAll() işlevini çağırmamış olsak bile coroutineScope oluşturucusunun, fetchTwoDocs işlevini çağıran eş yordamı, tüm yeni eş yordamlar tamamlanana kadar sürdürmediğini unutmayın.

Ayrıca, coroutineScope, eş yordamların oluşturduğu tüm istisnaları yakalar ve bunları arayana geri yönlendirir.

Paralel ayrıştırma hakkında daha fazla bilgi için Askıya alma işlevlerini oluşturma başlıklı makaleyi inceleyin.

Eş yordam kavramları

CoroutineScope

CoroutineScope launch veya async kullanarak oluşturduğu tüm eş yordamları takip eder. Devam eden çalışma (ör. çalışan eş yordamlar), herhangi bir zamanda scope.cancel() çağrılarak iptal edilebilir. Android'de bazı KTX kitaplıkları, belirli yaşam döngüsü sınıfları için kendi CoroutineScope'lerini sağlar. Örneğin, ViewModel için viewModelScope, Lifecycle için lifecycleScope geçerlidir. Ancak bir dağıtıcıdan farklı olarak CoroutineScope, eşzamanlı rutinleri çalıştırmaz.

viewModelScope, Android'de eş yordamlarla arka planda iş parçacığı oluşturma bölümündeki örneklerde de kullanılır. Ancak, uygulamanızın belirli bir katmanındaki eşzamanlı rutinlerin yaşam döngüsünü kontrol etmek için kendi CoroutineScope öğenizi oluşturmanız gerekiyorsa bunu aşağıdaki gibi yapabilirsiniz:

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

İptal edilen bir kapsam daha fazla eşzamanlı rutin oluşturamaz. Bu nedenle, scope.cancel() yalnızca yaşam döngüsünü kontrol eden sınıf yok edildiğinde çağrılmalıdır. viewModelScope kullanılırken ViewModel sınıfı, ViewModel'in onCleared() yönteminde kapsamı sizin için otomatik olarak iptal eder.

İş

Job, bir eş yordamın tanıtıcısıdır. launch veya async ile oluşturduğunuz her eş yordam, eş yordamı benzersiz şekilde tanımlayan ve yaşam döngüsünü yöneten bir Job örneği döndürür. Ayrıca, aşağıdaki örnekte gösterildiği gibi, yaşam döngüsünü daha iyi yönetmek için bir Job öğesini CoroutineScope öğesine de iletebilirsiniz:

class ExampleClass {
    // ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (condition) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

Bir CoroutineContext aşağıdaki öğe grubu kullanılarak bir eş yordamın davranışını tanımlar:

Bir kapsam içinde oluşturulan yeni eş yordamlar için yeni eş yordama yeni bir Job örneği atanır ve diğer CoroutineContext öğeleri kapsayan kapsamdan devralınır. Yeni bir CoroutineContext öğesini launch veya async işlevine ileterek devralınan öğeleri geçersiz kılabilirsiniz. Job öğesinin launch veya async öğesine iletilmesinin hiçbir etkisi olmadığını unutmayın. Çünkü yeni bir Job örneği her zaman yeni bir eşzamanlı işleme atanır.

class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

Ek eş yordam kaynakları

Daha fazla coroutine kaynağı için aşağıdaki bağlantılara bakın: