簡單介紹一下Android 協(xié)程

前言

最近新項目開始,老總發(fā)話說我們要用新技術(shù)践惑,不能再使用老的架構(gòu)和技術(shù)了。迫于無奈嘶卧,開始Google推薦的新架構(gòu)學習尔觉,基于單一數(shù)據(jù)源和單項數(shù)據(jù)流驅(qū)動的MVVM架構(gòu)。在學習的過程中芥吟,又系統(tǒng)的了解了一遍Android協(xié)程的使用侦铜。有了一些新的感悟,就記錄在此了钟鸵。 本文基本轉(zhuǎn)載自Android官方文檔钉稍,加了少許個人見解。大佬們看到請輕噴棺耍。

一贡未、協(xié)程的誕生

眾所周知,Android為了主線程安全蒙袍,是不能在主線程上去執(zhí)行任何耗時操作的俊卤。開發(fā)者在進行耗時操作時,需要自己啟動子線程后放在子線程中運行害幅,過程中會產(chǎn)生大量的線程管理代碼消恍。協(xié)程的誕生就是為了優(yōu)化這一操作,協(xié)程是一種并發(fā)的設(shè)計模式以现,可以在Android平臺上使用它來簡化需要異步執(zhí)行的代碼狠怨。

協(xié)程的特點

協(xié)程是Google推薦的在 Android 上進行異步編程的推薦解決方案。它具有一下特點:

  • 輕量:您可以在單個線程上運行多個協(xié)程邑遏,因為協(xié)程支持掛起佣赖,不會使正在運行協(xié)程的線程阻塞。掛起比阻塞節(jié)省內(nèi)存无宿,且支持多個并行操作茵汰。
  • 內(nèi)存泄漏更少:使用結(jié)構(gòu)化并發(fā)機制在一個作用域內(nèi)執(zhí)行多項操作。
  • 內(nèi)置取消支持:取消功能會自動通過正在運行的協(xié)程層次結(jié)構(gòu)傳播孽鸡。
  • Jetpack 集成:許多 Jetpack 庫都包含提供全面協(xié)程支持的擴展蹂午。某些庫還提供自己的協(xié)程作用域,可供您用于結(jié)構(gòu)化并發(fā)彬碱。

二豆胸、協(xié)程的使用

在后臺線程中執(zhí)行

如果在主線程上發(fā)出網(wǎng)絡請求,則主線程會處于等待或阻塞狀態(tài)巷疼,直到收到響應晚胡。由于線程處于阻塞狀態(tài),因此操作系統(tǒng)無法調(diào)用 onDraw(),這會導致應用凍結(jié)估盘,并有可能導致彈出“應用無響應”(ANR) 對話框瓷患。為了解決這個問題,通常開發(fā)中我們會在后臺線程上執(zhí)行網(wǎng)絡請求等耗時操作遣妥。

下面我們以一個簡單的登錄請求為例擅编,看一下協(xié)程操作的使用方法。

首先箫踩,我們先看一下在Google推薦的架構(gòu)中爱态,Repository 類是如何發(fā)出請求的:

//網(wǎng)絡請求響應結(jié)果實體封裝類
sealed class Result<out R> {
    //帶范型的返回數(shù)據(jù)類
    data class Success<out T>(val data: T) : Result<T>()
    //網(wǎng)絡請求錯誤結(jié)果數(shù)據(jù)類
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    //具體的請求地址
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    //具體的網(wǎng)絡請求函數(shù),會阻塞當前線程境钟,直到結(jié)果返回锦担。
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

上面的代碼中為了對網(wǎng)絡請求的響應數(shù)據(jù)做處理,我們創(chuàng)建了自己的 Result 類慨削。其中在 makeLoginRequest 是同步執(zhí)行函數(shù)洞渔,會阻塞發(fā)起調(diào)用的線程。

ViewModel 會在用戶與界面發(fā)生交互(例如理盆,點擊登錄按鈕)時觸發(fā)網(wǎng)絡請求:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

如果我們直接使用上述代碼痘煤,LoginViewModel 就會在網(wǎng)絡請求發(fā)出時阻塞界面線程。如需將執(zhí)行操作移出主線程猿规,我們以往的方式是啟動一個新的線程去執(zhí)行:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        //創(chuàng)建線程并啟動衷快,執(zhí)行登錄請求。
        Thread{
            Runnable {
                loginRepository.makeLoginRequest(jsonBody)
            }
        }.start()
    }
}



上面這樣的做法姨俩,會讓我們在每次執(zhí)行登網(wǎng)絡請求時都創(chuàng)建一個線程蘸拔,并且在請求完成后需要使用回調(diào)和handler把請求結(jié)果重新傳遞給主線程處理。而協(xié)程的出翔讓我們有個更簡單的方法环葵,就是創(chuàng)建一個新的協(xié)程调窍,然后在 I/O 線程上執(zhí)行網(wǎng)絡請求:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        //創(chuàng)建一個新的協(xié)程,使其移出UI線程執(zhí)行张遭, Dispatchers.IO: I/O 操作預留的線程
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            //執(zhí)行網(wǎng)絡請求操作邓萨,該請求會在I/O 操作預留的線程上執(zhí)行
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

下面我們仔細分析一下 login 函數(shù)中的協(xié)程代碼:

  • viewModelScope 是預定義的 CoroutineScope,包含在 ViewModel KTX 擴展中菊卷。請注意缔恳,所有協(xié)程都必須在一個作用域內(nèi)運行。一個 CoroutineScope 管理一個或多個相關(guān)的協(xié)程洁闰。
  • launch 是一個函數(shù)歉甚,用于創(chuàng)建協(xié)程并將其函數(shù)主體的執(zhí)行分派給相應的調(diào)度程序,(Dispatchers.IO) 為可選參數(shù)扑眉。
  • Dispatchers.IO 指示此協(xié)程應在為 I/O 操作預留的線程上執(zhí)行纸泄。

login 函數(shù)按以下方式執(zhí)行:

  • 應用從主線程上的 View 層調(diào)用 login 函數(shù)(點擊登錄按鈕)赖钞。
  • launch 會創(chuàng)建一個新的協(xié)程,并且網(wǎng)絡請求在為 I/O 操作預留的線程上獨立發(fā)出聘裁。
  • 在該協(xié)程運行時雪营,login 函數(shù)會繼續(xù)執(zhí)行,并可能在網(wǎng)絡請求完成前返回(請求不會阻塞主線程后續(xù)操作)咧虎。

由于此協(xié)程通過 viewModelScope 啟動卓缰,因此在 ViewModel 的作用域內(nèi)執(zhí)行。如果 ViewModel 因用戶離開屏幕而被銷毀砰诵,則 viewModelScope 會自動取消,且所有運行的協(xié)程也會被取消捌显。

前面的示例存在的兩個問題是茁彭,一是調(diào)用 makeLoginRequest 的任何項都需要記得將執(zhí)行操作顯式移出主線程,即在 launch 函數(shù)后傳入 (Dispatchers.IO) 參數(shù)扶歪。二是沒有對登錄請求的結(jié)果做處理理肺。下面我們來看看如何修改 Repository 以解決這一問題。

使用協(xié)程確保主線程安全

如果函數(shù)不會在主線程上阻止界面更新善镰,我們即將其視為是主線程安全的妹萨。makeLoginRequest 函數(shù)不是主線程安全的,因為從主線程調(diào)用 makeLoginRequest 確實會阻塞界面炫欺。在上面的代碼示例中乎完,我們可以在 ViewModel 中啟動協(xié)程,并分配對應的調(diào)度程序品洛,但是這種做法需要我們每次在調(diào)用 makeLoginRequest 時都要去尾貨調(diào)度程序树姨。為了解決該問題我們可以使用協(xié)程庫中的 withContext() 函數(shù)將協(xié)程的執(zhí)行操作移至其他線程:

class LoginRepository(...) {
    private const val loginUrl = "https://example.com/login"
    //suspend 關(guān)鍵字表示改方法會阻塞線程,Kotlin 利用此關(guān)鍵字強制從協(xié)程內(nèi)調(diào)用函數(shù)桥状。
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        // Move the execution of the coroutine to the I/O dispatcher
        //表示協(xié)程的后續(xù)執(zhí)行會被放在IO線程中
        return withContext(Dispatchers.IO) {
            val url = URL(loginUrl)
            (url.openConnection() as? HttpURLConnection)?.run {
                requestMethod = "POST"
                setRequestProperty("Content-Type", "application/json; utf-8")
                setRequestProperty("Accept", "application/json")
                doOutput = true
                outputStream.write(jsonBody.toByteArray())
                return Result.Success(responseParser.parse(inputStream))
            }
            return Result.Error(Exception("Cannot open HttpURLConnection"))
            
        }
    }
}

withContext(Dispatchers.IO) 將協(xié)程的執(zhí)行操作移至一個 I/O 線程帽揪,這樣一來,我們的調(diào)用函數(shù)便是主線程安全的辅斟,并且支持根據(jù)需要更新界面转晰。

makeLoginRequest 還會用 suspend 關(guān)鍵字進行標記。Kotlin 利用此關(guān)鍵字強制從協(xié)程內(nèi)調(diào)用函數(shù)士飒。

接下來我們在 ViewModel 中查邢,由于 makeLoginRequest 將執(zhí)行操作移出主線程,login 函數(shù)中的協(xié)程現(xiàn)在可以在主線程中執(zhí)行:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        //直接在UI主線程中啟動一個協(xié)程
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            //執(zhí)行網(wǎng)絡操作变汪,并且等待被suspend標記的函數(shù)執(zhí)行完成侠坎。
            //該等待并不會阻塞主線程,因為被suspend標記的函數(shù)會被分配到IO線程執(zhí)行
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            //當收到請求結(jié)果后裙盾,向用戶現(xiàn)實請求結(jié)果实胸,并更行對應界面
            when (result) {
                is Result.Success<LoginResponse> -> // 登錄成功他嫡,跳轉(zhuǎn)主頁。庐完。钢属。
                else -> // 登錄失敗,提示用戶錯誤信息
            }
        }
    }
}

請注意门躯,此處仍需要協(xié)程淆党,因為 makeLoginRequest 是一個 suspend 函數(shù),而所有 suspend 函數(shù)都必須在協(xié)程中執(zhí)行讶凉。

此代碼與前面的 login 示例的不同之處體現(xiàn)在以下幾個方面:

  • launch 不接受 (Dispatchers.IO) 參數(shù)染乌。默認從 viewModelScope 啟動的所有協(xié)程都會在主線程中運行。
  • 系統(tǒng)現(xiàn)在會處理網(wǎng)絡請求的結(jié)果懂讯,以顯示成功或失敗界面荷憋。

login 函數(shù)現(xiàn)在按以下方式執(zhí)行:

  • 應用從主線程上的 View 層調(diào)用 login() 函數(shù)。
  • launch 在主線程上創(chuàng)建新協(xié)程褐望,然后協(xié)程開始執(zhí)行勒庄。
  • 在協(xié)程內(nèi),調(diào)用 loginRepository.makeLoginRequest() 現(xiàn)在會掛起協(xié)程的進一步執(zhí)行操作瘫里,直至 makeLoginRequest() 中的 withContext 塊結(jié)束運行实蔽。
  • withContext 塊結(jié)束運行后,login() 中的協(xié)程在主線程上恢復執(zhí)行操作谨读,并返回網(wǎng)絡請求的結(jié)果局装。
  • 收到結(jié)果后,處理對應的結(jié)果并更行UI

處理異常

在進行網(wǎng)絡請求或者耗時操作時漆腌,經(jīng)常會拋出異常贼邓。為了處理 Repository 可能出現(xiàn)的異常,我們可以使用 try-catch 塊捕捉并處理對應異常:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun makeLoginRequest(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            //使用try-catch捕捉異常
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> // 登錄成功闷尿,跳轉(zhuǎn)主頁塑径。。填具。
                else -> // 登錄失敗统舀,提示用戶錯誤信息
            }
        }
    }
}

在上面代碼示例中,makeLoginRequest() 調(diào)用拋出的任何意外異常都會處理為界面錯誤劳景。

總結(jié)

這樣我們就使用協(xié)程完整實現(xiàn)了一個登錄請求的操作誉简,在此過程中,我們只需要在 loginRepository 中使用 withContext 函數(shù)聲明調(diào)度程序盟广,就可以避免耗時操作阻塞主線程的問題闷串,并且不需要開發(fā)者自己去管理對應的線程。并且因為有 viewModelScope 的存在筋量,使得我們也不需要去特意處理頁面銷毀后的請求取消問題烹吵。優(yōu)化性能的同時碉熄,又大大減少了我們的代碼量。是一種優(yōu)秀的異步代碼處理模式肋拔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锈津,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子凉蜂,更是在濱河造成了極大的恐慌琼梆,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窿吩,死亡現(xiàn)場離奇詭異茎杂,居然都是意外死亡,警方通過查閱死者的電腦和手機纫雁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門蛉顽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人先较,你說我怎么就攤上這事〉苛福” “怎么了闲勺?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長扣猫。 經(jīng)常有香客問我菜循,道長,這世上最難降的妖魔是什么申尤? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任癌幕,我火速辦了婚禮,結(jié)果婚禮上昧穿,老公的妹妹穿的比我還像新娘勺远。我一直安慰自己,他們只是感情好时鸵,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布胶逢。 她就那樣靜靜地躺著,像睡著了一般饰潜。 火紅的嫁衣襯著肌膚如雪初坠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天彭雾,我揣著相機與錄音碟刺,去河邊找鬼。 笑死薯酝,一個胖子當著我的面吹牛半沽,可吹牛的內(nèi)容都是我干的爽柒。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼抄囚,長吁一口氣:“原來是場噩夢啊……” “哼霉赡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起幔托,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤穴亏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后重挑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嗓化,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年刺覆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片史煎。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡谦屑,死狀恐怖篇梭,靈堂內(nèi)的尸體忽然破棺而出氢橙,到底是詐尸還是另有隱情,我是刑警寧澤恬偷,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布悍手,位于F島的核電站,受9級特大地震影響袍患,放射性物質(zhì)發(fā)生泄漏坦康。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一诡延、第九天 我趴在偏房一處隱蔽的房頂上張望滞欠。 院中可真熱鬧,春花似錦孕暇、人聲如沸仑撞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隧哮。三九已至,卻和暖如春座舍,著一層夾襖步出監(jiān)牢的瞬間沮翔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工曲秉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留采蚀,地道東北人疲牵。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像榆鼠,于是被迫代替她去往敵國和親纲爸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

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