請(qǐng)點(diǎn)贊,你的點(diǎn)贊對(duì)我意義重大台颠,滿足下我的虛榮心褐望。
??常在河邊走,哪有不濕鞋串前√崩铮或許面試過程中你遇到的問題就在這呢?
??關(guān)注我個(gè)人簡(jiǎn)介荡碾,面試不迷路~
一谨读、Kotlin內(nèi)置標(biāo)準(zhǔn)函數(shù)let的原理是什么?
這道題想考察什么坛吁?
- 是否了解Kotlin內(nèi)置標(biāo)準(zhǔn)函數(shù)let的原理是什么與真實(shí)場(chǎng)景使用劳殖,是否熟悉Kotlin內(nèi)置標(biāo)準(zhǔn)函數(shù)let的原理是什么本質(zhì)铐尚?
考察的知識(shí)點(diǎn)
- Kotlin內(nèi)置標(biāo)準(zhǔn)函數(shù)let的原理是什么的概念在項(xiàng)目中使用與基本知識(shí)
考生應(yīng)該如何回答
1.你工作這么些年,let內(nèi)置標(biāo)準(zhǔn)函數(shù)一般用的很頻繁吧哆姻,let的原理是什么宣增?
答:
使用端的感受:
1.在使用的時(shí)候,任何的類型填具,都可以.let出來(lái)使用统舀,這是為什么呢匆骗? 因?yàn)闃?biāo)準(zhǔn)let內(nèi)置函數(shù)內(nèi)部對(duì)泛型進(jìn)行了let函數(shù)擴(kuò)展劳景,意味著所有的類型都等于泛型,所以任何地方都是可以使用let函數(shù)的碉就。
2.所有類型.let {} 其實(shí)是一個(gè)匿名的Lambda表達(dá)式盟广,Lambda表達(dá)式的特點(diǎn)是,最后一行會(huì)自動(dòng)被認(rèn)為是返回值類型瓮钥,所以在表達(dá)式返回Boolean筋量,那么當(dāng)前的let函數(shù)就是Boolean類型,以此類推碉熄。
fun main() {
val r1 = "Derry".let {
true
it.length
}
println(r1)
val r2 = 123.let {
999
"【${it}】"
}
println(r2)
}
根據(jù)上面分析的兩點(diǎn)使用感受桨武,來(lái)分析他的原理:
1.inline : 是因?yàn)楹瘮?shù)有l(wèi)ambda表達(dá)式,屬于高階函數(shù)锈津,高階函數(shù)規(guī)范來(lái)說(shuō)要加inline
2.<T, R> T.let : T代表是要為T而擴(kuò)展出一個(gè)函數(shù)名let(任何類型都可以 萬(wàn)能類型.let)呀酸, R代表是Lambda表達(dá)式最后一行返回的類型
3.block: (T) -> R : Lambda表達(dá)式名稱block 輸入?yún)?shù)是T本身 輸出參數(shù)是R 也就是表達(dá)式最后一行返回推斷的類型
4.: R { : R代表是Lambda表達(dá)式最后一行返回的類型,若表達(dá)式返回類型是Boolean, 那么這整個(gè)let函數(shù)的返回類型就是Boolean
// inline : 是因?yàn)楹瘮?shù)有l(wèi)ambda表達(dá)式琼梆,屬于高階函數(shù)性誉,高階函數(shù)規(guī)范來(lái)說(shuō)要加inline
// <T, R> T.let : T代表是要為T而擴(kuò)展出一個(gè)函數(shù)名let(任何類型都可以 萬(wàn)能類型.let), R代表是Lambda表達(dá)式最后一行返回的類型
// block: (T) -> R : Lambda表達(dá)式名稱block 輸入?yún)?shù)是T本身 輸出參數(shù)是R 也就是表達(dá)式最后一行返回推斷的類型
// : R { : R代表是Lambda表達(dá)式最后一行返回的類型茎杂,若表達(dá)式返回類型是Boolean, 那么這整個(gè)let函數(shù)的返回類型就是Boolean
inline fun <T, R> T.let(block: (T) -> R): R {
println("你${this}.let在${System.currentTimeMillis()}這個(gè)時(shí)間點(diǎn)調(diào)用了我")
/*contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}*/
// 調(diào)用Lambda表達(dá)式
// 輸入?yún)?shù)this == T == "Derry" / 123,
// 輸出參數(shù):用戶返回String類型错览,就全部是返回String類型
return block(this)
}
總結(jié):Kotlin內(nèi)置標(biāo)準(zhǔn)let函數(shù),運(yùn)用了 高階函數(shù)特性與Lambda煌往,控制環(huán)節(jié)交給用戶完成倾哺,用戶在自己的Lambda表達(dá)式中,若返回Boolean刽脖,整個(gè)let函數(shù) 與 Lambda返回 都全部是Boolean
為了保證所有的類型都能正常使用let羞海,給泛型增加了擴(kuò)展函數(shù)let,所以所有的地方都可以使用let函數(shù)曾棕。
二扣猫、Kotlin語(yǔ)言的run高階函數(shù)的原理是什么?
這道題想考察什么翘地?
- 是否了解Kotlin語(yǔ)言的run高階函數(shù)的原理是什么與真實(shí)場(chǎng)景使用申尤,是否熟悉Kotlin語(yǔ)言的run高階函數(shù)的原理是什么本質(zhì)癌幕?
考察的知識(shí)點(diǎn)
- Kotlin語(yǔ)言的run高階函數(shù)的原理是什么的概念在項(xiàng)目中使用與基本知識(shí)
考生應(yīng)該如何回答
1.你工作這么些年,Kotlin語(yǔ)言提供的高階run函數(shù)一般用的很頻繁吧昧穿,run的原理是什么勺远?
答:
run在Kotlin語(yǔ)法中使用端的感受:
1.在使用的時(shí)候,任何的類型时鸵,都可以.run出來(lái)使用胶逢,這是為什么呢? 因?yàn)闃?biāo)準(zhǔn)run內(nèi)置函數(shù)內(nèi)部對(duì)泛型進(jìn)行run函數(shù)擴(kuò)展饰潜,意味著所有的類型都等于泛型初坠,所以任何地方都是可以使用run函數(shù)的。
2.所有類型.run{} 其實(shí)是一個(gè)匿名的Lambda表達(dá)式彭雾,Lambda表達(dá)式的特點(diǎn)是碟刺,最后一行會(huì)自動(dòng)被認(rèn)為是返回值類型,例如在表達(dá)式返回Boolean薯酝,那么當(dāng)前的run函數(shù)就是Boolean類型半沽,例如在表達(dá)式返回Int類型,那么當(dāng)前的run函數(shù)就是Int類型吴菠,以此類推者填。
fun main() {
val r1 : Int = "Derry".run {
true
length
}
println(r1)
val r2 : String = 123.run {
999
"【${it}】"
}
println(r2)
}
根據(jù)上面分析的兩點(diǎn)使用感受,來(lái)分析他的原理:
1.inline : 是因?yàn)楹瘮?shù)有l(wèi)ambda表達(dá)式做葵,屬于高階函數(shù)占哟,高階函數(shù)規(guī)范來(lái)說(shuō)要加inline
2.<T, R> T.run : T代表是要為T而擴(kuò)展出一個(gè)函數(shù)名run(任何類型都可以 萬(wàn)能類型.run), R代表是Lambda表達(dá)式最后一行返回的類型
3.block: T.() -> R : Lambda表達(dá)式名稱block 輸入?yún)?shù)是T本身 輸出參數(shù)是R 也就是表達(dá)式最后一行返回推斷的類型
4.: R { : R代表是Lambda表達(dá)式最后一行返回的類型蜂挪,若表達(dá)式返回類型是Boolean, 那么這整個(gè)run函數(shù)的返回類型就是Boolean
5.T.() 是讓lambda表達(dá)式里面持有了this(run函數(shù))重挑, (T) 是讓lambda表達(dá)式里面持有了it(let函數(shù))
/*
1.inline : 是因?yàn)楹瘮?shù)有l(wèi)ambda表達(dá)式,屬于高階函數(shù)棠涮,高階函數(shù)規(guī)范來(lái)說(shuō)要加inline
2.<T, R> T.run : T代表是要為T而擴(kuò)展出一個(gè)函數(shù)名run(任何類型都可以 萬(wàn)能類型.run)谬哀, R代表是Lambda表達(dá)式最后一行返回的類型
3.block: T.() -> R : Lambda表達(dá)式名稱block 輸入?yún)?shù)是T本身 輸出參數(shù)是R 也就是表達(dá)式最后一行返回推斷的類型
4.: R { : R代表是Lambda表達(dá)式最后一行返回的類型,若表達(dá)式返回類型是Boolean, 那么這整個(gè)run函數(shù)的返回類型就是Boolean
5.T.() 是讓lambda表達(dá)式里面持有了this(run函數(shù))严肪, (T) 是讓lambda表達(dá)式里面持有了it(let函數(shù))
*/
public inline fun <T, R> T.run(block: T.() -> R): R {
println("你${this}.run在${System.currentTimeMillis()}這個(gè)時(shí)間點(diǎn)調(diào)用了我")
/*contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}*/
// 調(diào)用Lambda表達(dá)式
// 輸入?yún)?shù)this == T == "Derry" / 123,
// 輸出參數(shù):用戶返回String類型史煎,就全部是返回String類型
return block()
}
總結(jié):Kotlin內(nèi)置標(biāo)準(zhǔn)run函數(shù),運(yùn)用了 高階函數(shù)特性與Lambda驳糯,控制環(huán)節(jié)交給用戶完成篇梭,用戶在自己的Lambda表達(dá)式中,若返回Boolean酝枢,整個(gè)run函數(shù) 與 Lambda返回 都全部是Boolean
為了保證所有的類型都能正常使用run恬偷,給泛型增加了擴(kuò)展函數(shù)run,所以所有的地方都可以使用run函數(shù)帘睦。
三袍患、Kotlin語(yǔ)言泛型的形變是什么坦康?
這道題想考察什么?
- 是否了解Kotlin語(yǔ)言泛型的形變是什么與真實(shí)場(chǎng)景使用诡延,是否熟悉Kotlin語(yǔ)言泛型的形變是什么本質(zhì)滞欠?
考察的知識(shí)點(diǎn)
- Kotlin語(yǔ)言泛型的形變是什么的概念在項(xiàng)目中使用與基本知識(shí)
考生應(yīng)該如何回答
1.你工作這么些年,對(duì)于Kotlin語(yǔ)言泛型的形變是什么肆良,有了解么筛璧?
答:
形變一共分為三個(gè)區(qū)域:不變,協(xié)變惹恃,逆變
不變
不變指的是:這個(gè)泛型夭谤,可以是生產(chǎn)者,也可以是消費(fèi)者座舍,此泛型沒有任何泛型繼承相關(guān)的概念沮翔,可以理解是完全獨(dú)立出來(lái)的泛型
例如:下面案例中,此泛型既可以是生產(chǎn)者曲秉,也可以是消費(fèi)者
// 不變
class StudentSetGets<IO> {
private var item : IO? = null
// 消費(fèi)者
fun set(value : IO) {
println("你傳遞進(jìn)來(lái)的內(nèi)容是:$value")
item = value
}
// 生產(chǎn)者
fun get() = item
}
協(xié)變
協(xié)變指的是,這個(gè)泛型疲牵,只能是生產(chǎn)者承二,此泛型有泛型繼承相關(guān)的概念存在,可以理解此泛型纲爸,可以接收此泛型類型的子類型
例如:下面案例中亥鸠,此泛型只能是生產(chǎn)者,說(shuō)白了识啦,只能給用戶端负蚊,讀取泛型,卻不能修改泛型
class MyStudentGet<out T>(_item : T) {
private val item = _item
fun get() : T = item
}
逆變
逆變指的是颓哮,這個(gè)泛型家妆,只能是消費(fèi)者,此泛型有泛型父類轉(zhuǎn)子類的強(qiáng)轉(zhuǎn)相關(guān)的概念存在冕茅,可理解此泛型伤极,可以接收此泛型類型的父類型
例如:下面案例中,此泛型只能是消費(fèi)者姨伤,說(shuō)白了哨坪,只能給用戶端,修改泛型乍楚,卻不能讀取泛型
class MyStudentSet<in T>() {
fun set(value: T) = println("你傳遞進(jìn)來(lái)的內(nèi)容是:$value")
}
結(jié)論
為什么協(xié)變只能讀取泛型当编,不能修改泛型?
答:因?yàn)?例如<Object> = <String> 泛型接收端是Object徒溪,而泛型具體端是String忿偷,由于具體端有很多很多Object的子類拧篮,
而泛型會(huì)被泛型擦除,所以無(wú)法明確你到底要修改那個(gè)子類啊
為什么逆變只能修改泛型牵舱,不能讀取泛型串绩?
答:因?yàn)?例如<String> = <Object> 泛型接收端是String,而泛型具體端是Object芜壁,由于接收端是String礁凡,而讀取時(shí),
會(huì)讀取到String的父類慧妄,但是接收端是String顷牌,你卻讀取到String的父類,這個(gè)本來(lái)就是不合理的
四塞淹、Kotlin協(xié)程在工作中有用過嗎窟蓝?
這道題想考察什么?
理解協(xié)程的目的是饱普,簡(jiǎn)化復(fù)雜的異步代碼邏輯运挫,用同步的代碼寫出復(fù)雜的異步代碼邏輯。
考察的知識(shí)點(diǎn)
kotlin套耕、協(xié)程谁帕、線程、并發(fā)
考生應(yīng)該如何回答
1.你工作這么些年冯袍,對(duì)于Kotlin語(yǔ)言協(xié)程是什么匈挖,有了解么?
答:
雖然對(duì)于一些人來(lái)說(shuō)比如剛開始的我康愤,協(xié)程(Coroutines) 是一個(gè)新的概念儡循,但是協(xié)程這個(gè)術(shù)語(yǔ)早在1958年就被提出并用于構(gòu)建匯編程序,協(xié)程是一種編程思想征冷,并不局限于特定的語(yǔ)言择膝,就像Rx也是一種思想,并不局限于使用Java實(shí)現(xiàn)的RxJava资盅。不同語(yǔ)言實(shí)現(xiàn)的協(xié)程庫(kù)可能名稱或者使用上有所不同调榄,但它們的設(shè)計(jì)思想是有相似之處的。
kotlinx.coroutines是由JetBrains開發(fā)的kotlin協(xié)程庫(kù)呵扛,可以把它簡(jiǎn)單的理解為一個(gè)線程框架 每庆。但是協(xié)程不是線程,也不是新版本的線程今穿,它是基于線程封裝的一套更上層工具庫(kù)缤灵,我們可以使用kotlin協(xié)程庫(kù)提供的api方便的靈活的指定協(xié)程中代碼執(zhí)行的線程、切換線程,但是不需要接觸線程Thread類腮出。說(shuō)到這里帖鸦,大家可能就會(huì)想到Android的AsyncTask或者RxJava的Schedulers,沒錯(cuò)胚嘲,從某種意義上來(lái)說(shuō)它們和協(xié)程是相通的作儿,都解決了異步線程切換的問題,然而協(xié)程最重要的是通過非阻塞掛起和恢復(fù)實(shí)現(xiàn)了異步代碼的同步編寫方式馋劈,把原本運(yùn)行在不同線程的代碼寫在一個(gè)代碼塊{}里攻锰,看起來(lái)就像是同步代碼。
// 注意:在真實(shí)開發(fā)過程中妓雾,MainScope作用域用的非常常用
MainScope().launch(){ // 注意:此協(xié)程塊默認(rèn)是在UI線程中啟動(dòng)協(xié)程
// 下面的代碼看起來(lái)會(huì)以同步的方式一行行執(zhí)行(異步代碼同步獲取結(jié)果)
val token = apiService.getToken() // 網(wǎng)絡(luò)請(qǐng)求:IO線程娶吞,獲取用戶token
val user = apiService.getUser(token)// 網(wǎng)絡(luò)請(qǐng)求:IO線程,獲取用戶信息
nameTv.text = user.name // 更新 UI:主線程械姻,展示用戶名
val articleList = apiService.getArticleList(user.id)// 網(wǎng)絡(luò)請(qǐng)求:IO線程妒蛇,根據(jù)用戶id獲取用戶的文章集合哦
articleTv.text = "用戶${user.name}的文章頁(yè)數(shù)是:${articleList.size}頁(yè)" // 更新 UI:主線程
}
協(xié)程并不是從操作系統(tǒng)層面創(chuàng)立的新的運(yùn)行方式,代碼是運(yùn)行在線程中的楷拳,線程又是運(yùn)行在進(jìn)程中的绣夺,協(xié)程也是運(yùn)行在線程中的,所以才說(shuō)它是基于線程封裝的庫(kù)唯竹。然而有人會(huì)拿協(xié)程與線程比較乐导,問協(xié)程是不是比線程效率更高?如果理解了協(xié)程是基于線程封裝就應(yīng)該知道浸颓,協(xié)程并沒有改變代碼運(yùn)行在線程中的原則,單線程中的協(xié)程執(zhí)行時(shí)間并不會(huì)比不用協(xié)程少旺拉,它們之間沒有可比性产上,因?yàn)樗鼈兏静粚儆谕活愂挛铮粎f(xié)程也不是為了線程而生的蛾狗,它是為了解決因?yàn)槎嗑€程帶來(lái)的編碼上的不便的問題而出現(xiàn)的晋涣。
2.那這樣說(shuō)的話,協(xié)程到底有什么用沉桌?
在Android開發(fā)中谢鹊,通常會(huì)將耗時(shí)操作放到子線程中,然后通過回調(diào)的方式將結(jié)果返回后切換主線程更新UI留凭,但是實(shí)際開發(fā)過程中可能遇到很多奇怪而合理的需求佃扼,它們可能是:
一個(gè)頁(yè)面需要同時(shí)并發(fā)請(qǐng)求多個(gè)接口,當(dāng)所有接口都請(qǐng)求完成需要做一些合并處理然后更新UI 按照慣例蔼夜,我們可能會(huì)為每個(gè)接口請(qǐng)求設(shè)置一個(gè)boolean標(biāo)志兼耀,每當(dāng)一個(gè)接口請(qǐng)求完將對(duì)應(yīng)的boolean值改為true,當(dāng)最后一個(gè)接口請(qǐng)求完成發(fā)現(xiàn)所有標(biāo)志都為true再更新UI,這樣就能達(dá)到并發(fā)請(qǐng)求的目的瘤运,然而管理這么多boolean值累不累窍霞??jī)?yōu)雅不優(yōu)雅? 初級(jí)程序員可能干脆來(lái)個(gè)單線程拯坟,一個(gè)接口請(qǐng)求完成后但金,再請(qǐng)求另一個(gè)接口,直到最后一個(gè)接口返回?cái)?shù)據(jù)郁季,玩暴力美學(xué)啊冷溃,本來(lái)能同時(shí)干的事情非得一件件干,你讓用戶浪費(fèi)他寶貴的時(shí)間合適嗎巩踏?浪費(fèi)用戶高配的性能過癮嗎秃诵? 高級(jí)一點(diǎn)的可能就上RxJava了,通過RxJava的zip操作符塞琼,達(dá)到發(fā)射一次菠净,將結(jié)果合并處理的目的,但是說(shuō)實(shí)話到現(xiàn)在還有很多人不會(huì)用
RxJava 先調(diào)用接口1獲取數(shù)據(jù)彪杉,然后拿到接口1的結(jié)果作為參數(shù)調(diào)用接口2毅往,然后將接口2的數(shù)據(jù)展示出來(lái) 按照慣例,我們可能會(huì)調(diào)用接口1派近,然后在接口1的回調(diào)中獲取數(shù)據(jù)再嵌套調(diào)用接口2 高級(jí)一點(diǎn)的可能就上RxJava了
會(huì)引發(fā)回調(diào)地獄問題:
/**RetrofitClient單例*/
object RetrofitClient {
/**log**/
private val logger = HttpLoggingInterceptor.Logger {
FLog.i(this::class.simpleName, it)
}
private val logInterceptor = HttpLoggingInterceptor(logger).apply {
level = HttpLoggingInterceptor.Level.BODY
}
/**OkhttpClient*/
private val okHttpClient = OkHttpClient.Builder()
.callTimeout(10, TimeUnit.SECONDS)
.addNetworkInterceptor(logInterceptor)
.build()
/**Retrofit*/
private val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl(ApiService.BASE_URL)
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
/**ApiService*/
val apiService: ApiService = retrofit.create(ApiService::class.java)
}
/**接口定義*/
interface ApiService {
companion object {
const val BASE_URL = "https://www.wanandroid.com"
}
/*獲取文章樹結(jié)構(gòu)*/
@GET("tree/json")
fun getTree(): Call<ApiResult<MutableList<Tree>>>
/*根據(jù)數(shù)結(jié)構(gòu)下某個(gè)分支id攀唯,獲取分支下的文章*/
@GET("article/list/{page}/json")
fun getArticleList(
@Path("page") page: Int,
@Query("cid") cid: Int
): Call<ApiResult<Pagination<Article>>>
}
/**ViewModel*/
class SystemViewModel : BaseViewModel(){
private val remoteRepository : SystemRemoteRepository by lazy { SystemRemoteRepository() }
val page = MutableLiveData<Pagination<Article>>()
fun getArticleList() {
remoteRepository.getArticleList(){
page.value = it
}
}
}
/**數(shù)據(jù)倉(cāng)庫(kù)*/
class SystemRemoteRepository{
/**
* 1. 展示回調(diào)嵌套,回調(diào)地獄
*/
fun getArticleList(responseBack: (result: Pagination<Article>?) -> Unit) {
/**1. 獲取文章樹結(jié)構(gòu)*/
val call:Call<ApiResult<MutableList<Tree>>> = RetrofitClient.apiService.getTree()
//同步(需要自己手動(dòng)切換線程)
//val response : Response<ApiResult<MutableList<Tree>>> = call.execute()
//異步回調(diào)
call.enqueue(object : Callback<ApiResult<MutableList<Tree>>> {
override fun onFailure(call: Call<ApiResult<MutableList<Tree>>>, t: Throwable) {
}
override fun onResponse(call: Call<ApiResult<MutableList<Tree>>>, response: Response<ApiResult<MutableList<Tree>>>) {
FLog.v("請(qǐng)求文章樹結(jié)構(gòu)成功:"+response.body())
/**2. 獲取分支id下的第一頁(yè)文章*/
val treeid = response.body()?.data?.get(0)?.id
//當(dāng)treeid不為null執(zhí)行
treeid?.let {
RetrofitClient.apiService.getArticleList(0, treeid)
.enqueue(object : Callback<ApiResult<Pagination<Article>>> {
override fun onFailure(call: Call<ApiResult<Pagination<Article>>>, t: Throwable) {
}
override fun onResponse(call: Call<ApiResult<Pagination<Article>>>, response: Response<ApiResult<Pagination<Article>>>) {
//返回獲取的文章列表
responseBack(response.body()?.data)
}
})
}
}
})
}
}
其實(shí)上面數(shù)據(jù)倉(cāng)庫(kù)中的方法是應(yīng)該拆分為2個(gè)方法的渴丸,第二個(gè)接口請(qǐng)求拆分為方法后還可以復(fù)用(分頁(yè)獲群钹帧),這里僅僅為了展示嵌套回調(diào)的需求谱轨〗溽#可以看到僅僅是兩層回調(diào)嵌套,可讀性就已經(jīng)很差了土童,然而這種需求并非不常見的甚至?xí)霈F(xiàn)更多層嵌套诗茎,這時(shí)候就會(huì)寫出深>形的代碼,非常不雅觀而且不易于代碼復(fù)用及后期維護(hù)献汗。
有人會(huì)說(shuō)將上面的嵌套回調(diào)拆分為2個(gè)方法敢订,在第一個(gè)接口請(qǐng)求完成之后再調(diào)用另一個(gè)方法請(qǐng)求文章列表,這不就消除了嵌套回調(diào)了嗎罢吃?從視覺上來(lái)說(shuō)確實(shí)是消除了楚午,但是從邏輯上來(lái)說(shuō)嵌套依然存在,而且這種方式會(huì)讓兩個(gè)方法之間形成很強(qiáng)的業(yè)務(wù)關(guān)聯(lián)刃麸,對(duì)代碼維護(hù)帶來(lái)的挑戰(zhàn)不比嵌套回調(diào)小醒叁。代碼就不展示了,就是簡(jiǎn)單的將兩個(gè)請(qǐng)求拆分。
Rx解決回調(diào)地獄
使用Retrofit+RxJava組合通過Rx的鏈?zhǔn)秸{(diào)用就能消除嵌套回調(diào):
interface ApiService {
...
/**RxJava方式*/
@GET("tree/json")
fun getTreeByRx(): Observable<ApiResult<MutableList<Tree>>>
@GET("article/list/{page}/json")
fun getArticleListByRx(
@Path("page") page: Int,
@Query("cid") cid: Int
): Observable<ApiResult<Pagination<Article>>>
}
class SystemRemoteRepository{
/**
* 2. Retrofit+RxJava消除回調(diào)嵌套
*/
fun getArticleListByRx(responseBack: (result: Pagination<Article>?) -> Unit) {
/**1. 獲取文章樹結(jié)構(gòu)*/
val observable1: Observable<ApiResult<MutableList<Tree>>> = RetrofitClient.apiService.getTreeByRx()
observable1.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
//使用當(dāng)前Observable發(fā)出的值調(diào)用給定的Consumer把沼,然后將其轉(zhuǎn)發(fā)給下游
.doOnNext({
FLog.v("1請(qǐng)求文章樹成功啊易,切換到主線程處理數(shù)據(jù):${Thread.currentThread()}")
})
.observeOn(Schedulers.io())
.flatMap({
FLog.v("2請(qǐng)求文章樹成功,IO線程中獲取接口1的數(shù)據(jù)饮睬,然后將被觀察者變換為接口2的Observable:${Thread.currentThread()}")
if(it?.errorCode == 0){
//當(dāng)treeid不為null執(zhí)行
it?.data?.get(0)?.id?.let { it1 -> RetrofitClient.apiService.getArticleListByRx(0, it1) }
}else{
//請(qǐng)求錯(cuò)誤的情況租谈,發(fā)射一個(gè)Error
Observable.error({
Throwable("獲取文章樹失敗:${it.errorCode}:${it.errorMsg}")
})
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object: Observer<ApiResult<Pagination<Article>>> {
override fun onComplete() {}
override fun onSubscribe(d: Disposable?) {}
override fun onNext(t: ApiResult<Pagination<Article>>?) {
FLog.v("3請(qǐng)求文章列表成功:${t?.data}")
responseBack(t?.data)
}
override fun onError(e: Throwable?) {
FLog.e("3請(qǐng)求失斃Τ睢:${e?.message}")
}
})
}
}
Retrofit+RxJava確實(shí)消除了回調(diào)的嵌套割去,但是還是避免不了回調(diào)(Observer觀察者可看作是回調(diào)),鏈?zhǔn)秸{(diào)用處理異步數(shù)據(jù)流確實(shí)比傳統(tǒng)的嵌套回調(diào)好了太多昼丑,但是代碼量不減反增呻逆,而且我們需要在正確的位置準(zhǔn)確的插入不同的操作符用來(lái)處理異步數(shù)據(jù),對(duì)于不熟悉Rx的同學(xué)來(lái)說(shuō)也是很頭痛的菩帝,所以還不是很好咖城,下面將出現(xiàn)協(xié)程來(lái)解決
協(xié)程來(lái)解決此問題:
使用協(xié)程就可以讓我們擺脫因?yàn)槎嗑€程帶來(lái)的各種編碼上的不便:
class SystemViewModel : BaseViewModel(){
private val remoteRepository : SystemRemoteRepository by lazy { SystemRemoteRepository() }
val page = MutableLiveData<Pagination<Article>>()
fun getArticleList() {
viewModelScope.launch { //主線程開啟一個(gè)協(xié)程
// 網(wǎng)絡(luò)請(qǐng)求:IO線程
val tree : ApiResult<MutableList<Tree>> = RetrofitClient.apiService.getTreeByCoroutines()
// 主線程
val cid = tree?.data?.get(0)?.id
if(cid!=null){
// 網(wǎng)絡(luò)請(qǐng)求:IO線程
val pageResult : ApiResult<Pagination<Article>> = RetrofitClient.apiService.getArticleListByCoroutines(0, cid)
// 主線程
page.value = pageResult.data!!
}
}
}
}
/**接口定義,Retrofit從2.6.0版本開始支持協(xié)程*/
interface ApiService {
/*獲取文章樹結(jié)構(gòu)*/
@GET("tree/json")
suspend fun getTreeByCoroutines(): ApiResult<MutableList<Tree>>
/*根據(jù)數(shù)結(jié)構(gòu)下某個(gè)分支id呼奢,獲取分支下的文章*/
@GET("article/list/{page}/json")
suspend fun getArticleListByCoroutines(
@Path("page") page: Int,
@Query("cid") cid: Int
): ApiResult<Pagination<Article>>
}
運(yùn)行上面的代碼宜雀,盡然成功了,剛剛發(fā)生了什么握础?這不就是同步調(diào)用嗎辐董?跟上面的同步有什么不一樣嗎?看起來(lái)差不多啊禀综,確實(shí)差不多简烘,就是在定義接口時(shí),方法前加了個(gè)suspend關(guān)鍵字定枷,調(diào)用接口的時(shí)候用viewModelScope.launch {}包裹夸研。既然可以運(yùn)行成功,就說(shuō)明請(qǐng)求接口并不是在主線程中進(jìn)行的依鸥,然而有的同學(xué)不信,他在getArticleList()方法中的任意位置通過Thread.currentThread()打印的結(jié)果都是Thread[main,5,main]悼沈,這不就是在主線程中調(diào)用的嗎贱迟?上述協(xié)程中的代碼是在主線程執(zhí)行沒錯(cuò),但是接口請(qǐng)求的方法卻是在子線程中執(zhí)行的絮供。
原因就在于我們定義接口的時(shí)候使用了suspend關(guān)鍵字衣吠,它的意思是掛起、暫停壤靶,函數(shù)被加了這個(gè)標(biāo)記就稱它為掛起函數(shù)缚俏,需要注意的是,suspend關(guān)鍵字并沒有其他重要的作用,它僅僅標(biāo)識(shí)某個(gè)函數(shù)是掛起函數(shù)忧换,可以在函數(shù)中調(diào)用其他掛起函數(shù)恬惯,但是只能在協(xié)程中調(diào)用它。所以上面兩個(gè)接口都被定義為掛起函數(shù)了亚茬,掛起函數(shù)只能在協(xié)程中調(diào)用酪耳,那誰(shuí)是協(xié)程?
其實(shí)在kotlin協(xié)程庫(kù)中是有一個(gè)類AbstractCoroutine來(lái)表示協(xié)程的刹缝,這個(gè)抽象類有很多子類代表不同的協(xié)程碗暗,但是這些子類都是private的,并沒有暴露給我們梢夯,所以你在其他文章中看到別人說(shuō)viewModelScope.launch{}包裹起來(lái)的閉包(代碼塊)就是協(xié)程也沒問題言疗,但這個(gè)代碼塊的真正意義是協(xié)程需要執(zhí)行的代碼。當(dāng)在協(xié)程中調(diào)用到掛起函數(shù)時(shí)颂砸,協(xié)程就會(huì)在當(dāng)前線程(主線程)中被掛起噪奄,這就是協(xié)程中著名的非阻塞式掛起,主線程暫時(shí)停止執(zhí)行這個(gè)協(xié)程中剩余的代碼沾凄,注意:暫停并不是阻塞等待(否則會(huì)ANR)梗醇,而是主線程暫時(shí)從這個(gè)協(xié)程中被釋放出來(lái)去處理其他Handler消息,比如響應(yīng)用戶操作撒蟀、繪制View等等叙谨。
那掛起函數(shù)誰(shuí)執(zhí)行?這得看掛起函數(shù)內(nèi)部是否有切換線程保屯,如果沒有切換線程當(dāng)然就是主線程執(zhí)行了手负,所以掛起函數(shù)不一定就是在子線程中執(zhí)行的,但是通常在定義掛起函數(shù)時(shí)都會(huì)為它指定其他線程姑尺,這樣掛起才有意義竟终。比如上面定義的suspend的請(qǐng)求接口,Retorift在執(zhí)行請(qǐng)求的時(shí)候就切換到了子線程并掛起主線程切蟋,當(dāng)請(qǐng)求完成(掛起函數(shù)執(zhí)行完畢)返回結(jié)果時(shí)统捶,會(huì)通知主線程:我該干的都干完了,下面的事你接著干吧柄粹,主線程接到通知后就會(huì)拿到掛起函數(shù)返回的結(jié)果繼續(xù)執(zhí)行協(xié)程里面剩余的代碼喘鸟,這叫做協(xié)程恢復(fù)(resume)。如果又遇到掛起函數(shù)就會(huì)重復(fù)這個(gè)過程驻右,直到協(xié)程中的代碼被執(zhí)行完什黑。
通過協(xié)程可以徹底去除回調(diào),使用同步的方式編寫異步代碼堪夭。什么是同步調(diào)用梨树?調(diào)用一個(gè)方法能直接拿到方法的返回值,盡管這個(gè)方法是耗時(shí)的础浮、在其他線程執(zhí)行的,也能直接得到它的返回值嚣镜,然后再執(zhí)行下面的代碼,協(xié)程不是通過等待的方式實(shí)現(xiàn)同步圣絮,而是通過非阻塞掛起實(shí)現(xiàn)看起來(lái)同步的效果祈惶。
總結(jié):
協(xié)程的目的是,簡(jiǎn)化復(fù)雜的異步代碼邏輯扮匠,用同步的代碼寫出復(fù)雜的異步代碼邏輯捧请。
今天的面試分享到此結(jié)束拉~下期在見