網(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等。

總結(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í)成本