全面總結(jié)Android面試知識(shí)要點(diǎn):Kotlin 核心面試題

請(qǐng)點(diǎn)贊,你的點(diǎn)贊對(duì)我意義重大台颠,滿足下我的虛榮心褐望。
??常在河邊走,哪有不濕鞋串前√崩铮或許面試過程中你遇到的問題就在這呢?
??關(guān)注我個(gè)人簡(jiǎn)介荡碾,面試不迷路~

一谨读、Kotlin內(nèi)置標(biāo)準(zhǔn)函數(shù)let的原理是什么?

這道題想考察什么坛吁?

  1. 是否了解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)

  1. 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ù)的原理是什么?

這道題想考察什么翘地?

  1. 是否了解Kotlin語(yǔ)言的run高階函數(shù)的原理是什么與真實(shí)場(chǎng)景使用申尤,是否熟悉Kotlin語(yǔ)言的run高階函數(shù)的原理是什么本質(zhì)癌幕?

考察的知識(shí)點(diǎn)

  1. 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ǔ)言泛型的形變是什么坦康?

這道題想考察什么?

  1. 是否了解Kotlin語(yǔ)言泛型的形變是什么與真實(shí)場(chǎng)景使用诡延,是否熟悉Kotlin語(yǔ)言泛型的形變是什么本質(zhì)滞欠?

考察的知識(shí)點(diǎn)

  1. 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é)束拉~下期在見

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市棒搜,隨后出現(xiàn)的幾起案子疹蛉,更是在濱河造成了極大的恐慌,老刑警劉巖力麸,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件可款,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡克蚂,警方通過查閱死者的電腦和手機(jī)闺鲸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)埃叭,“玉大人摸恍,你說(shuō)我怎么就攤上這事〕辔荩” “怎么了立镶?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)类早。 經(jīng)常有香客問我媚媒,道長(zhǎng),這世上最難降的妖魔是什么涩僻? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任缭召,我火速辦了婚禮,結(jié)果婚禮上逆日,老公的妹妹穿的比我還像新娘恼琼。我一直安慰自己,他們只是感情好屏富,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛙卤,像睡著了一般狠半。 火紅的嫁衣襯著肌膚如雪噩死。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天神年,我揣著相機(jī)與錄音已维,去河邊找鬼。 笑死已日,一個(gè)胖子當(dāng)著我的面吹牛垛耳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播飘千,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼堂鲜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了护奈?” 一聲冷哼從身側(cè)響起缔莲,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎霉旗,沒想到半個(gè)月后痴奏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厌秒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年读拆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸵闪。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡檐晕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出岛马,到底是詐尸還是另有隱情棉姐,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布啦逆,位于F島的核電站伞矩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏夏志。R本人自食惡果不足惜乃坤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沟蔑。 院中可真熱鬧湿诊,春花似錦、人聲如沸瘦材。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)食棕。三九已至朗和,卻和暖如春错沽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背眶拉。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工千埃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人忆植。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓放可,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親朝刊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子耀里,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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

  • 最近有換工作打算,能用kotlin做項(xiàng)目和能回答面試官關(guān)于kotlin的問題是兩回事坞古, 于是就在網(wǎng)上搜了搜相關(guān)面試...
    CyrusChan閱讀 30,490評(píng)論 19 31
  • 1.1 請(qǐng)簡(jiǎn)述一下什么是 Kotlin备韧?它有哪些特性? kotlin和java一樣也是一門jvm語(yǔ)言最后的編譯結(jié)果...
    星邪Ara閱讀 15,626評(píng)論 2 39
  • 從公眾號(hào)拿來(lái)的大廠面試題痪枫,忘記哪個(gè)公號(hào)了织堂,前段時(shí)間面試做了一部分,時(shí)間不夠很多都只簡(jiǎn)單過了一遍奶陈。題目很全易阳,建議都過...
    三天過去了閱讀 881評(píng)論 0 5
  • 1. 重點(diǎn)理解val的使用規(guī)則 引用1 如果說(shuō)var代表了varible(變量),那么val可看成value(值)...
    leeeyou閱讀 502評(píng)論 0 0
  • Java 一吃粒、順序表和鏈表的區(qū)別++++ 順序表 順序表是用一段物理地址連續(xù)的存儲(chǔ)單元依次存儲(chǔ)數(shù)據(jù)元素的線性結(jié)構(gòu)潦俺,...
    溫溫溫888閱讀 465評(píng)論 0 0