Android Kotlin Coroutine(1):入門(mén)篇

Kotlin Coroutine 一般翻譯成協(xié)程,顧名思義可以理解成協(xié)作程序吹害,它并不是 Kotlin 特有的,很多程序都有協(xié)程這個(gè)概念虚青。剛開(kāi)始接觸時(shí)它呀,對(duì)這些概念還是挺費(fèi)解的。我在這里試圖從0開(kāi)始棒厘,講講怎么理解協(xié)程這個(gè)概念钟些,并把它應(yīng)用到我們的 Android 應(yīng)用程序開(kāi)發(fā)中來(lái)。萬(wàn)事開(kāi)頭難绊谭,按照慣例政恍,我也用協(xié)程來(lái)寫(xiě)一個(gè) Hello World 出來(lái)。

1. 添加Android協(xié)程依賴(lài)庫(kù)

在 Android 中要使用協(xié)程达传,首先需要引入 Kotlin 的基礎(chǔ)庫(kù)和協(xié)程庫(kù)篙耗,kotlin 對(duì)協(xié)程的支持都在 kotlinx.coroutines包中,在 build.gradle 中添加依賴(lài):

//kotlin基礎(chǔ)庫(kù)和協(xié)程庫(kù)
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'

注意宪赶,在寫(xiě)這篇文章時(shí)宗弯,kotlinx-coroutines-android 的版本為1.3.2,同時(shí)要求 kotlin 的版本為1.3.50搂妻。

為了后面通過(guò)網(wǎng)絡(luò)請(qǐng)求舉例來(lái)說(shuō)明協(xié)程的作用蒙保,同時(shí)引入 retrofitrxjava 庫(kù):

api 'com.squareup.retrofit2:retrofit:2.6.2'
api 'io.reactivex.rxjava2:rxjava:2.2.13'
api 'io.reactivex.rxjava2:rxandroid:2.1.1'
api 'com.squareup.retrofit2:adapter-rxjava2:2.6.2'
api 'com.squareup.retrofit2:converter-gson:2.6.2'

Android 開(kāi)發(fā)如果不知道 retrofit 和 rxjava 是什么,那么請(qǐng)自行搜索學(xué)習(xí)了之后再往下閱讀欲主。這里需要注意的是邓厕,retrofit 早一點(diǎn)的版本,對(duì)協(xié)程的新特性還不支持扁瓢。

2. 第一個(gè)協(xié)程程序

萬(wàn)事俱備只欠東風(fēng)详恼,隨便寫(xiě)一個(gè)點(diǎn)擊事件,執(zhí)行以下代碼:

println("1.測(cè)試開(kāi)始引几,創(chuàng)建協(xié)程")
GlobalScope.launch {
    println("2.協(xié)程開(kāi)始運(yùn)行")
    delay(3000)
    println("3.協(xié)程內(nèi)部延遲3s后")
    withContext(Dispatchers.Main) {
        println("4.toast Hello World")
        Toast.makeText(this@MainActivity, "Hello World", Toast.LENGTH_SHORT).show()
    }
}
println("5.協(xié)程外部運(yùn)行")

執(zhí)行結(jié)果如下:

1.測(cè)試開(kāi)始昧互,創(chuàng)建協(xié)程
5.協(xié)程外部運(yùn)行
2.協(xié)程開(kāi)始運(yùn)行
3.協(xié)程內(nèi)部延遲3s后
4.toast Hello World

多次執(zhí)行,執(zhí)行結(jié)果也可能為:

1.測(cè)試開(kāi)始伟桅,創(chuàng)建協(xié)程
2.協(xié)程開(kāi)始運(yùn)行
5.協(xié)程外部運(yùn)行
3.協(xié)程內(nèi)部延遲3s后
4.toast Hello World

執(zhí)行結(jié)果里敞掘,"2" 和 "5" 的順序可能會(huì)變化,其他都不變楣铁。到這里玖雁,可能你會(huì)比較懵逼,協(xié)程到底是個(gè)什么鬼民褂,執(zhí)行順序怎么還會(huì)變化茄菊,先不用管這些疯潭,你只需要知道通過(guò) GlobalScope.launch() 方法啟動(dòng)了一個(gè)協(xié)程就行了赊堪。至此面殖,我們已經(jīng)很直觀(guān)的感受到了協(xié)程。

3. 采用Retrofit來(lái)進(jìn)行一次網(wǎng)絡(luò)接口請(qǐng)求

首先問(wèn)一個(gè)問(wèn)題哭廉,接口請(qǐng)求成功后脊僚,怎么將結(jié)果通知出去?目前常見(jiàn)的有2種方式:

  1. 通過(guò) callback 回調(diào)的方式遵绰;
  2. 使用 rxjava 類(lèi)似的框架辽幌;

那我們先來(lái)寫(xiě)一個(gè)例子,采用 retrofit 模擬做一個(gè)登錄請(qǐng)求:

//接口定義
interface TestService {
    @GET("https://www.baidu.com")
    fun testLogin(): Call<UserInfo>

    @GET("https://www.baidu.com")
    fun testLoginEx(): Flowable<UserInfo>
}

data class UserInfo(val name: String, val age: Int) {
    override fun toString(): String {
        return "UserInfo(name='$name', age=$age)"
    }
}

val testApi: Retrofit by lazy {
    val okHttpClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            val builder = Response.Builder()
            //為了測(cè)試方便椿访,模擬返回?cái)?shù)據(jù)
            val respBytes = "{\"name\":\"hjy\",\"age\":30}".toByteArray()
            val source = Okio.source(ByteArrayInputStream(respBytes))
            //模擬網(wǎng)絡(luò)請(qǐng)求乌企,線(xiàn)程睡眠3秒鐘
            Thread.sleep(3000)
            //隨機(jī)返回正確結(jié)果或者錯(cuò)誤結(jié)果
            builder.code(if ((Math.random() * 10).toInt() % 2 == 0) 200 else 400)
                .message("")
                .request(chain.request())
                .protocol(Protocol.HTTP_1_1)
                .body(RealResponseBody("application/json", respBytes.size.toLong(), Okio.buffer(source)))
            builder.build()
        }
        .build()
    val retrofitBuilder = Retrofit.Builder()
        .client(okHttpClient)
        .baseUrl("https://wwww.baidu.com")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    retrofitBuilder.build()
}

熟悉 retrofit、okhttp 的成玫,這段代碼應(yīng)該都能看得懂加酵,為了方便測(cè)試,我們本地模擬隨機(jī)返回正確的結(jié)果或者返回異常哭当,后面很多實(shí)際案例如不做特殊說(shuō)明猪腕,會(huì)以此為基礎(chǔ)。

如果采用回調(diào)的方式來(lái)調(diào)用接口钦勘,常規(guī)的寫(xiě)法是:

val call = testApi.create(TestService::class.java).testLogin()
val handler = Handler(Looper.getMainLooper())
call.enqueue(object : retrofit2.Callback<UserInfo> {
    override fun onFailure(call: Call<UserInfo>, t: Throwable) {
        handler.post {
            t.printStackTrace()
        }
    }

    override fun onResponse(call: Call<UserInfo>, response: Response<UserInfo>) {
        handler.post {
            if (response.isSuccessful) {
                println("callback 回調(diào)結(jié)果:${response.body()}")
            } else {
                println("請(qǐng)求失斅稀:errorCode = ${response.code()}")
            }
        }
    }
})

如果采用 rxjava 的方式來(lái)調(diào)用接口,常規(guī)的寫(xiě)法是:

testApi.create(TestService::class.java)
    .testLoginEx().subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe({ userinfo ->
        println("調(diào)用成功:${userinfo}")
    }, { exception ->
        println(exception.message)
    })

上面這樣的代碼彻采,相信每個(gè)人都寫(xiě)過(guò)成百上千遍了腐缤,那么我根據(jù)經(jīng)驗(yàn)來(lái)評(píng)價(jià)下這2種方式:

  1. 回調(diào)的方式處理接口請(qǐng)求已是上古時(shí)代的產(chǎn)物了,在 Android 中更新 UI 必須在主線(xiàn)程中執(zhí)行肛响,所以最終相當(dāng)于嵌套了 2 個(gè) callback柴梆,一不小心就會(huì)產(chǎn)生回調(diào)地獄;
  2. rxjava 相比回調(diào)的方式已經(jīng)優(yōu)雅很多了终惑,在沒(méi)有使用協(xié)程之前斗幼,可以認(rèn)為是目前的最優(yōu)方案疆股。但是如果你用過(guò) map、flatMap 等操作符,一些眼花繚亂的操作雄妥,雖然功能很強(qiáng)大,但對(duì)開(kāi)發(fā)者來(lái)說(shuō)真的是轴咱,如果能不用就不用吧实苞;
  3. 如果有多個(gè)異步請(qǐng)求需要串行執(zhí)行或者并行請(qǐng)求,這2種方式寫(xiě)起來(lái)都很蛋疼质帅;

4. 使用協(xié)程來(lái)請(qǐng)求接口

先定義接口适揉,需要注意的是 retrofit 2.6.0 開(kāi)始才能直接支持協(xié)程留攒,老版本是不直接支持的,需要其他方式來(lái)實(shí)現(xiàn)嫉嘀,我這里略過(guò)不介紹了:

@GET("https://www.baidu.com")
suspend fun testLoginByCoroutine(): UserInfo

與 rxjava 的接口相比炼邀,增加了 suspend 關(guān)鍵字,返回結(jié)果直接是返回的對(duì)象剪侮,去掉了 Flowable 的包裝拭宁,再看如何調(diào)用:

MainScope().launch {
    try {
        val userInfo = testApi.create(TestService::class.java).testLoginByCoroutine()
        //以下結(jié)果會(huì)在主線(xiàn)程執(zhí)行
        println("調(diào)用成功:$userInfo")
    } catch (e: Exception) {
        //結(jié)果會(huì)在主線(xiàn)程執(zhí)行
        println(e.message)
    }
}

這里通過(guò) MainScope().launch(...) 啟動(dòng)了一個(gè)協(xié)程,MainScope() 是類(lèi)似 GlobalScope 的東西瓣俯,只不過(guò)它默認(rèn)是在主線(xiàn)程執(zhí)行杰标,現(xiàn)在可以先不用理會(huì)它。令人驚訝的是彩匕,協(xié)程體內(nèi)的代碼居然都是順序執(zhí)行腔剂,完全沒(méi)有了回調(diào),并且以上代碼并不會(huì)阻塞主線(xiàn)程驼仪,且返回的結(jié)果也是在主線(xiàn)程掸犬,現(xiàn)在居然線(xiàn)程切換的代碼都不用了。

以同步的方式來(lái)寫(xiě)異步的代碼谅畅,代碼簡(jiǎn)單登渣,邏輯清晰明了,有了它我再也不想用 rxjava 了毡泻。熟悉 javascript 的同學(xué)肯定了解胜茧,這不是與 js 里的 async、await 有異曲同工之妙么仇味。

5. 使用協(xié)程來(lái)請(qǐng)求多個(gè)接口

如果上面這個(gè)例子呻顽,你看不出協(xié)程有多大優(yōu)勢(shì)的話(huà),那么我們來(lái)看個(gè)更復(fù)雜的例子丹墨。假設(shè)我們有這樣一個(gè)業(yè)務(wù)場(chǎng)景:第一步需要先調(diào)用登錄接口廊遍,如果登錄成功則需要從服務(wù)端獲取某些信息,然后才能進(jìn)行后面的操作贩挣。也就是說(shuō)需要先后調(diào)用2個(gè)接口喉前,并且前一個(gè)接口的結(jié)果會(huì)影響后面的流程。同樣我們使用協(xié)程來(lái)模擬這種情況:

MainScope().launch {
    try {
        val testService = testApi.create(TestService::class.java)
        //第一次調(diào)用
        val userInfo1 = testService.testLoginByCoroutine()
        println("第一個(gè)接口調(diào)用成功:$userInfo1")
        //前面接口調(diào)用成功后王财,調(diào)用另一個(gè)接口 
        val userInfo2 = testService.testLoginByCoroutine()
        println("第二個(gè)接口調(diào)用成功:$userInfo2")
    } catch (e: Exception) {
        println("出現(xiàn)異常:${e.message}")
    }
}

出現(xiàn)的結(jié)果可能會(huì)有:

第一個(gè)接口調(diào)用成功:UserInfo(name='hjy', age=30)
第二個(gè)接口調(diào)用成功:UserInfo(name='hjy', age=30)

或者

出現(xiàn)異常:HTTP 400

或者

第一個(gè)接口調(diào)用成功:UserInfo(name='hjy', age=30)
出現(xiàn)異常:HTTP 400

至少到這里卵迂,我可以說(shuō)使用協(xié)程有這些好處:

  1. 幫助你告別回調(diào)地獄;
  2. 優(yōu)雅的線(xiàn)程切換绒净,甚至讓你無(wú)感知见咒;
  3. 統(tǒng)一異常處理入口,降低異常處理的復(fù)雜度挂疆;

6. 協(xié)程到底是什么改览?

前面這些例子下翎,如果你親手敲一遍代碼,并執(zhí)行看看結(jié)果宝当,那么你對(duì)協(xié)程就會(huì)有很直觀(guān)的感受了视事。那么協(xié)程到底是什么呢?

一個(gè)操作系統(tǒng)可管理多個(gè)進(jìn)程今妄,一個(gè)進(jìn)程可管理多個(gè)線(xiàn)程郑口。平時(shí)我們寫(xiě) Java 時(shí)鸳碧,所謂的異步任務(wù)都是運(yùn)行在線(xiàn)程當(dāng)中的盾鳞。而線(xiàn)程的切換,最典型的是 Android 中瞻离,我們?cè)?IO 線(xiàn)程請(qǐng)求接口腾仅,接口返回成功后需要切換到主線(xiàn)程更新 UI。這種一個(gè)進(jìn)程中線(xiàn)程的切換套利,都是由操作系統(tǒng)來(lái)調(diào)度的推励,程序并不好控制。

協(xié)程是一種比線(xiàn)程更輕量級(jí)的存在肉迫,網(wǎng)上很多人說(shuō)協(xié)程是輕量級(jí)的線(xiàn)程验辞,我覺(jué)得這種說(shuō)法不靠譜,很容易誤導(dǎo)人喊衫。協(xié)程并不是線(xiàn)程跌造,它就是一種特殊的函數(shù):它可以在某個(gè)地方掛起,并且可以在以后的某個(gè)時(shí)間點(diǎn)族购,重新在掛起處繼續(xù)運(yùn)行壳贪。協(xié)程的執(zhí)行最終靠的還是線(xiàn)程,應(yīng)用程序來(lái)調(diào)度協(xié)程選取合適的線(xiàn)程來(lái)獲取執(zhí)行權(quán)寝杖。把應(yīng)用程序比作操作系統(tǒng)违施,協(xié)程就像線(xiàn)程一樣,應(yīng)用程序可以自己來(lái)操作和調(diào)度協(xié)程的運(yùn)行瑟幕。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末磕蒲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子只盹,更是在濱河造成了極大的恐慌辣往,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹿霸,死亡現(xiàn)場(chǎng)離奇詭異排吴,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)懦鼠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)钻哩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)屹堰,“玉大人,你說(shuō)我怎么就攤上這事街氢〕都” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵珊肃,是天一觀(guān)的道長(zhǎng)荣刑。 經(jīng)常有香客問(wèn)我,道長(zhǎng)伦乔,這世上最難降的妖魔是什么厉亏? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮烈和,結(jié)果婚禮上爱只,老公的妹妹穿的比我還像新娘。我一直安慰自己招刹,他們只是感情好恬试,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著疯暑,像睡著了一般训柴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妇拯,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天幻馁,我揣著相機(jī)與錄音,去河邊找鬼乖阵。 笑死宣赔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瞪浸。 我是一名探鬼主播儒将,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼对蒲!你這毒婦竟也來(lái)了钩蚊?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蹈矮,失蹤者是張志新(化名)和其女友劉穎砰逻,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體泛鸟,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蝠咆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刚操。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闸翅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菊霜,到底是詐尸還是另有隱情坚冀,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布鉴逞,位于F島的核電站记某,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏构捡。R本人自食惡果不足惜液南,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叭喜。 院中可真熱鬧贺拣,春花似錦蓖谢、人聲如沸捂蕴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)啥辨。三九已至,卻和暖如春盯腌,著一層夾襖步出監(jiān)牢的瞬間溉知,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工腕够, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留级乍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓帚湘,卻偏偏與公主長(zhǎng)得像玫荣,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子大诸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容