Try   HackMD

Android的threading與asynchronous

概要

為了良好的使用者體驗,Android對thread的限制十分嚴格(主要是UI thread),每次太久沒碰Android都會忘記該怎麼用,這邊簡單筆記一下
如果想直接看官方Android threading的doc的話就點這裡

最重要且不太會忘記的部分應該就是UI thread不能被block住,只要block超過五秒左右系統就會跳沒有APP回應的對話視窗
接著就是thread或是asynchronous要怎麼產生怎麼用,主要就分成以下幾種機制

  1. Thread
  2. AsyncTask (deprecated in API level 30)
  3. Coroutines (Kotlin)
  4. Rx (RxJava)

其實AsyncTask我有寫過記得滿好用的,但既然要被淘汰了那就算了XD
Rx感覺要學一下,好像是跟Observer pattern和asynchronous有關,但我還沒有用過,所以這次筆記不會寫
以下就針對Thread跟Coroutines做簡單筆記

Process and Thread in Android

在開始之前,先來講一下Android中process和thread的關係。
預設情況下,每個app會有屬於自己的一個Linux user ID,且每個app會在一個自己的Linux process中運行,然後所有component(activity, service, broadcast receiver和content provider)的function(像是onCreate()等等以及自己寫的function)都是同步地跑在main thread中。
以上都是預設的情況,你也可以透過更改AndroidManifest中各個component的android:process名字來設定各個component要用哪個process;或是透過Thread、Coroutines等機制來切換使用不同的thread以及異步的處理。

Thread

這裡有兩種thread

  1. Thread: 一次性,做完就關閉
  2. HandlerThread: 可重複使用,做完就待命

關於Thread的用法,官方的教學系列文在這邊
HandlerThread的部分以及Handler、thread pool等等的我就先不寫了,改天我有要用到再回來補充,這邊我就只放上Thread的用法(從官網抓來的)來了解一下就好。Thread很簡單,如下:

fun onClick(v: View) {
    Thread(Runnable {
        // a potentially time consuming task
        val bitmap = processBitMap("image.png")
        imageView.post {
            imageView.setImageBitmap(bitmap)
        }
    }).start()
}

首先你要有一個Runable當作Thread的引數,然後直接叫這個Thread做run()。然後他就開始跑了,跑完就關閉,就這麼簡單。

如果對Kotlin還不熟的話可以注意一下,這個start()不是Thread的static method,因為Kotlin沒有new這個keyword,所以這邊Thread()其實已經是一個新的instance了,這個start()是這個新instance所執行的method。

Runable的部分它其實是一個interface,上面的程式碼是直接放一個anonymous class作為建構Thread的引述(不是new一個interface喔),但你也可以在其他地方寫一個class來實作Runable,這樣比較好重複使用。

Coroutines

coroutine是Kotlin的東西,如果你要查相關的document的話要去Kotlin那邊查,現在在Android開發中常常會用到。它基本用起來算簡單好用,但他背後若要細細探究也是可以花上很多時間,以下只介紹基本的用法,比較深的我自己也沒鑽研。
簡單來說,couroutine就是在一個thread中實現異步的一個機制,核心的class(明確地說是以下大寫的都是interface)有幾個:

  1. CoroutineScope
    就是coroutine存在的範圍,官方頁面在此
  2. CoroutineContext
    就是這個coroutine的使用場景(情境、環境),是CoroutineScope的一個property,讓coroutine builders知道現在的場景是什麼。具體來說它是一個像是map的東西,裡面放了些coroutine context elements(明確來說是Element這個interface)。CoroutineContext裡面通常會放像是Job跟CoroutineDispatcher的Element,它們都是繼承Element的interface。可以透過this.coroutineContext[Job]這種方式來access該context裡面的各個Job或是其他Element。更多細節可以看官方頁面,而這篇SO回答也寫得很好。
  3. coroutine builders
    是用來創造以及操作coroutine的function,其實就是CoroutineScope的一些public method,像是launch、async和cancel等等,會直接使用該CoroutineScope的context。
  4. Job
    就是coroutine中實際在背景被執行的程式碼片段,也就是coroutine本人。感覺起來有點像Java的Runable,但我覺得Runable比較強調run,而Job比較強調cancel。Job是繼承Element(coroutine context的element)的一個interface,所以可以被放在CoroutineContext中,官方頁面在此,分為兩種Job:
    • Coroutine job:
      由launch所產生的Job,當該程式碼片段執行完後即completed。
    • CompletableJob:
      由Job()這個factory function所產生的Job,要調用CompletableJob.complete才會使它completed。
  5. CoroutineDispatcher: CoroutineDispatcher決定了這個coroutine會在哪個thread上面運行。官方頁面在此,而更多細節可以看這個官方說明。實際在使用上,我們會用Kotlin提供的Dispatchers這個class,以下簡單介紹:
    • Dispatchers.Default:
      這是預設使用的CoroutineDispatcher,他會在背景的thread pool裡面挑一個thread來用,是worker thread不是main thread。適合CPU密集的coroutine。
    • Dispatchers.IO
      這也是從背景的thread pool裡面挑一個worker thread來用,適合IO密集的coroutine。(這裡的超連結是空的,因為點IO好像會產生超連結我弄不掉XD)
    • Dispatchers.Main:
      這個會讓coroutine在main thread中運行。這個好像是Android特有的,需要Android相關的dependency。

在coroutine基本的元件都知道後,接下來就可以看程式了。
以下介紹兩種用法,第一種是用GlobalScope,不建議使用,但因為寫法太簡單通常都是剛開始學Coroutine時必學的東西,程式碼如下:

import ...

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // launch test
        GlobalScope.launch {
            println("I'm here!")
        }
    }
}

可以發現基本上就是加了個GlobalScope.launch{}而已,而這邊要講一下GlobalScope,他是實作CoroutineScope,要使用coroutine的時候一定要有這個CoroutineScope才行,因為這個scope定義了coroutine可運行的範圍,你可以把這個scope跟lifecycle綁在一起(就是待會要介紹的第二種用法)、跟ViewModel綁一起或是跟LiveData綁在一起。而不建議使用GlobalScope的原因是它是top-level的,就算你的Activity或Fragment被砍掉了GlobalScope依舊存在,所以要留好它的reference來手動停止它。此外它也會使用較多的記憶體。

下面介紹第二種,我把CoroutineScrope跟Activity的lifecycle綁在一起(就是讓Activity去實作CoroutineScope),也很簡單,如果要用基本的coroutine就直接照下面的用吧:

import ...

class MainActivity : AppCompatActivity(), CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Job()
        
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // launch test
        launch {
            println("I'm here!")
            println("${this.coroutineContext.isActive}")
        }
        // launch using specific dispatcher
        launch(Dispatchers.IO) {
            val url = "https://xxx.com/api"
            val client = OkHttpClient();
            val request = Request.Builder()
                .url(url)
                .build()
            try {
                val response = client.newCall(request).execute()
                println(response)
            } catch (e: IOException) {
                println(e)
            }
        }
    }
}

這邊放了一個Job作為coroutine的context,這也是官方說傳統上會使用的方法,讓並發更結構化(structured concurrency實在是太難翻譯了QQ),而不是粗暴地使用top-level的GlobalScope。
上面程式碼當中,第一個launch沒有指定dispatcher,所以會用Dispatchers.Default,而第二個launch因為有指定,所以會用Dispatchers.IO(這邊這個超連結也是空的,一樣是因為點IO)。

好目前就先筆記到這樣,之後如果有新內容再補充。