Android協(xié)程的使用

網(wǎng)絡(luò)請(qǐng)求

現(xiàn)在比較流行的網(wǎng)絡(luò)框架,就是retrofit,而且retrofit從2.6版本開(kāi)始,實(shí)現(xiàn)了對(duì)協(xié)程的支持,其實(shí)可以理解為retrofit對(duì)suspend關(guān)鍵字的支持。
以前如果是使用retrofit來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求,一般都有這么幾個(gè)步驟:1、初始化retrofit 2、初始化Api代理接口 3、請(qǐng)求并在回調(diào)中處理結(jié)果

    private fun initRetrofit() {
        val okHttpClient = OkHttpClient.Builder().sslSocketFactory(
            TrustAllSSLSocketFactory.newInstance(),
            TrustAllSSLSocketFactory.TrustAllCertsManager()
        )
        retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .client(okHttpClient.build())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        api = retrofit.create(GitHubApi::class.java)
    }
interface GitHubApi {
    @GET("users/{user}/repos")
    fun listRepos(@Path("user")user:String):Call<List<Repo>>
}

    private fun requestByNormal() {
        if (::retrofit.isInitialized && ::api.isInitialized) {
            api.listRepos("TonyDash")
                .enqueue(object : Callback<List<Repo>> {
                    override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
                        textView.text = "requestByNormal onFailure"
                    }

                    override fun onResponse(
                        call: Call<List<Repo>>,
                        response: Response<List<Repo>>
                    ) {
                        textView.text = response.body()?.get(0)?.name
                    }

                })
        }
    }

這樣就完成了一次網(wǎng)絡(luò)請(qǐng)求了。那如果使用協(xié)程的話,需要怎么實(shí)現(xiàn)呢?其實(shí),在上面的基礎(chǔ)上稍作修改,就可以變成協(xié)程,初始化retrofit部分不需要改動(dòng),先改api接口的定義:

interface GitHubApi {
    @GET("users/{user}/repos")
    fun listRepos(@Path("user")user:String):Call<List<Repo>>
    @GET("users/{user}/repos")
    suspend fun listReposKt(@Path("user")user:String):List<Repo>
}

listRepoKt就是支持協(xié)程的api,可以看到,區(qū)別就只有方法的前面多了一個(gè)suspend關(guān)鍵字,suspend的作用就是標(biāo)記這個(gè)為掛起函數(shù)。最后來(lái)看請(qǐng)求部分:

    private fun requestByKt() {
        if (::retrofit.isInitialized && ::api.isInitialized) {
            GlobalScope.launch(Dispatchers.Main) {
                val repos = api.listReposKt("TonyDash")
                textView.text ="KT${repos[0].name}"
            }
        }
    }

這樣就利用協(xié)程完成了一次網(wǎng)絡(luò)請(qǐng)求了??梢悦黠@看到,在請(qǐng)求數(shù)據(jù)的部分,少了回調(diào),只需要肉眼看上去的按順序?qū)?,就完成了異線程執(zhí)行網(wǎng)絡(luò)請(qǐng)求并主線程更新UI控件的工作,單個(gè)請(qǐng)求可能感覺(jué)差異不大,但是如果有一個(gè)需求你必須請(qǐng)求多個(gè)接口,并且多個(gè)接口還是有因果關(guān)系的,那就會(huì)有一種回調(diào)地獄的感覺(jué),而且后期維護(hù)起來(lái)也相對(duì)麻煩,萬(wàn)一忘了以前是為什么這么寫(xiě)的呢?還有如果細(xì)心的話可以發(fā)現(xiàn),利用回調(diào)處理結(jié)果的請(qǐng)求方法,有一個(gè)onFailure來(lái)處理請(qǐng)求的異常情況,那協(xié)程呢?協(xié)程怎么處理異常,下面我們單獨(dú)講。

異常處理

使用try catch來(lái)捕捉異常,由于kotlin取消了check exception機(jī)制,所以要捕捉異常,我們只能使用try catch來(lái)捕捉協(xié)程內(nèi)的異常。

    private fun requestByKt() {
        if (::retrofit.isInitialized && ::api.isInitialized) {
            GlobalScope.launch(Dispatchers.Main) {
                try {
                    val repos = api.listReposKt("TonyDash")
                    textView.text = "KT${repos[0].name}"
                } catch (e: Exception) {
                    textView.text = e.message ?: "error"
                }
            }
        }
    }
RxJava與協(xié)程

網(wǎng)絡(luò)請(qǐng)求,一般都會(huì)使用retrofit、rxjava、OKhttp等。首先不管用沒(méi)用過(guò),我們要明白這些東西是要來(lái)做什么的,其實(shí)retrofit和OKhttp基本一樣,你可以理解為網(wǎng)絡(luò)代理,就是你告訴它調(diào)用哪個(gè)接口,它返回你結(jié)果,至于過(guò)程,如果你沒(méi)那個(gè)需要,不管也行。至于rxjava,可以理解為一個(gè)工具箱,主要用于多線程并發(fā)任務(wù)的管理,還有事件流。那先用rxjava實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求怎么做?

interface GitHubApi {
    fun listReposRx(@Path("user")user:String): Single<List<Repo>>
}

返回值修改為Single,就完成了api的修改了。

    private fun initRetrofit() {
        val okHttpClient = OkHttpClient.Builder().sslSocketFactory(
            TrustAllSSLSocketFactory.newInstance(),
            TrustAllSSLSocketFactory.TrustAllCertsManager()
        )
        retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .client(okHttpClient.build())
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))//使用rxjava是加上這一句
            .build()
        api = retrofit.create(GitHubApi::class.java)
    }

同時(shí)初始化retrofit是,需要加上addCallAdapterFactory,這里我默認(rèn)為全部的網(wǎng)絡(luò)請(qǐng)求,都在io線程執(zhí)行。

    private fun requestByRx() {
        if (::retrofit.isInitialized && ::api.isInitialized) {
            api.listReposRx("TonyDash")
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(object : SingleObserver<List<Repo>> {
                    override fun onSuccess(t: List<Repo>) {
                        textView.text = "Rx${t[0].name}"
                    }

                    override fun onSubscribe(d: Disposable) {
                        textView.text = "onSubscribe"
                    }

                    override fun onError(e: Throwable) {
                        textView.text = e.message?:"onError"
                    }
                })
        }
    }

最后就是請(qǐng)求部分,大致上跟傳統(tǒng)的回調(diào)方式類似,多的部分就是對(duì)于線程切換的指定,利用observeOn指定了結(jié)果再主線程執(zhí)行,結(jié)果也同樣是在回調(diào)方法中處理。這樣一看感覺(jué)變化不大,但是如果有個(gè)需求,需要2個(gè)接口,把2個(gè)接口的結(jié)果顯示出來(lái)呢?
如果不用rxjava,也能做,就是結(jié)果嵌套,在上一個(gè)接口的結(jié)果回調(diào)中,繼續(xù)執(zhí)行下一個(gè)接口的邏輯。這樣的寫(xiě)法,看上去就相對(duì)的麻煩,而且不清晰。
如果使用協(xié)程,應(yīng)該怎么寫(xiě)?

    private fun requestByKtAsync(){
        if (::retrofit.isInitialized && ::api.isInitialized) {
            GlobalScope.launch(Dispatchers.Main) {
                try {
                    val async1 = async { api.listReposKt("TonyDash") }
                    val async2 = async { api.listReposKt("TonyDash") }
                    textView.text = "${async1.await()[0].name} requestByKtAsync ${async2.await()[0].name}"
                }catch (e:Exception){
                    textView.text = e.message ?: "error"
                }
            }
        }
    }

這里可以看到,使用了async,這里的async其實(shí)也是一個(gè)協(xié)程,而await()就是把方法掛起了,等到2個(gè)請(qǐng)求都有結(jié)果了,再賦值給textview。
可以看到協(xié)程的優(yōu)勢(shì)是:簡(jiǎn)潔,去掉了回調(diào);還有就是代碼的寫(xiě)法上,相對(duì)的簡(jiǎn)單。
相同的地方就是:大家都可以切換線程;都不需要嵌套調(diào)用。

協(xié)程的缺點(diǎn)

我個(gè)人的觀點(diǎn),凡是越簡(jiǎn)單好用的,就等于別人幫你做了更多的事情,也就是說(shuō),性能損耗會(huì)大一些,例如suspend關(guān)鍵字,其實(shí)就是如何找回對(duì)應(yīng)的線程進(jìn)行處理邏輯,其實(shí)是一個(gè)復(fù)雜的過(guò)程,就會(huì)相對(duì)地有性能的損耗,但是這個(gè)損耗相比于協(xié)程帶來(lái)的好處,我覺(jué)得可以忽略。
協(xié)程能必須使用try catch來(lái)捕捉異常,我個(gè)人覺(jué)得這里也算是一個(gè)小小的麻煩吧,不算缺點(diǎn)。

協(xié)程泄漏

還有一個(gè)常見(jiàn)的場(chǎng)景我們需要注意,我們的耗時(shí)操作都是維持一段時(shí)間的,那如果這段時(shí)間內(nèi),用戶把a(bǔ)ctivity關(guān)閉了,因?yàn)榫W(wǎng)絡(luò)請(qǐng)求內(nèi)部是一個(gè)活躍的線程,并且持有activity的對(duì)象,那這個(gè)就會(huì)造成內(nèi)存泄漏。所以在不用的時(shí)候,我們應(yīng)該把協(xié)程取消掉。

    private fun requestByKtAsync(){
        if (::retrofit.isInitialized && ::api.isInitialized) {
            jobKtAsync = GlobalScope.launch(Dispatchers.Main) {
                try {
                    val async1 = async { api.listReposKt("TonyDash") }
                    val async2 = async { api.listReposKt("TonyDash") }
                    textView.text = "${async1.await()[0].name} requestByKtAsync ${async2.await()[0].name}"
                }catch (e:Exception){
                    textView.text = e.message ?: "error"
                }
            }
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        if (::jobKt.isInitialized){
            jobKt.cancel()
        }
        if (::jobKtAsync.isInitialized){
            jobKtAsync.cancel()
        }
    }
CoroutineScope

上面的代碼,一直用到都是GlobalScope.launch,我也說(shuō)過(guò)GlobalScope其實(shí)一般用于調(diào)試,實(shí)際上不這么寫(xiě),而且這樣不方便管理,我們可以創(chuàng)建一個(gè)全局統(tǒng)一管理的協(xié)程,在ondestroy的時(shí)候,統(tǒng)一取消協(xié)程。

class PracticeActivity2 : AppCompatActivity() {
    ...
    private val mainScope = MainScope()
    ...
}
    private fun requestByKtAsync(){
        if (::retrofit.isInitialized && ::api.isInitialized) {
            mainScope.launch(Dispatchers.Main) {
                try {
                    val async1 = async { api.listReposKt("TonyDash") }
                    val async2 = async { api.listReposKt("TonyDash") }
                    textView.text = "${async1.await()[0].name} requestByKtAsync ${async2.await()[0].name}"
                }catch (e:Exception){
                    textView.text = e.message ?: "error"
                }
            }
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        mainScope.cancel()
    }

除此之外,我們甚至能使用jetpack來(lái)更加方便地使用,利用ktx的擴(kuò)展屬性。

    // Lifecycles only (without ViewModel or LiveData)
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
    private fun requestByKt() {
        if (::retrofit.isInitialized && ::api.isInitialized) {
            lifecycleScope.launch(Dispatchers.Main) {
                try {
                    val repos = api.listReposKt("TonyDash")
                    textView.text = "KT${repos[0].name}"
                } catch (e: Exception) {
                    textView.text = e.message ?: "error"
                }
            }
        }
    }

這樣使用lifecycleScope來(lái)啟動(dòng)協(xié)程,甚至連ondestroy中都不需要我們手動(dòng)去取消協(xié)程,因?yàn)閗otlin已經(jīng)幫我們做了。另外ktx還有一些方便的api方法給我們使用,例如launchWhenCreated、launchWhenResumed、launchWhenStarted等。


image.png
總結(jié):

協(xié)程和線程分別是什么?
對(duì)于kotlin for android而言,協(xié)程就是一個(gè)線程的框架,用于處理并發(fā)任務(wù)的。
協(xié)程和線程的優(yōu)缺點(diǎn)?
優(yōu)勢(shì):好用、簡(jiǎn)潔、去掉了回調(diào)使得邏輯清晰,而且自動(dòng)切換線程。
缺點(diǎn):相對(duì)的新,需要學(xué)習(xí)成本

例子demo:https://github.com/TonyDash/coroutines.git

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容