精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

在 Android 開(kāi)發(fā)中使用協(xié)程 | 代碼實(shí)戰(zhàn)

移動(dòng)開(kāi)發(fā) Android
本文是介紹 Android 協(xié)程系列中的第三部分,這篇文章通過(guò)發(fā)送一次性請(qǐng)求來(lái)介紹如何使用協(xié)程處理在實(shí)際編碼過(guò)程中遇到的問(wèn)題。

本文是介紹 Android 協(xié)程系列中的第三部分,這篇文章通過(guò)發(fā)送一次性請(qǐng)求來(lái)介紹如何使用協(xié)程處理在實(shí)際編碼過(guò)程中遇到的問(wèn)題。在閱讀本文之前,建議您先閱讀本系列的前兩篇文章,關(guān)于在 Android 開(kāi)發(fā)中使用協(xié)程的背景介紹上手指南

[[332592]]

使用協(xié)程解決實(shí)際編碼問(wèn)題

前兩篇文章主要是介紹了如何使用協(xié)程來(lái)簡(jiǎn)化代碼,在 Android 上保證主線程安全,避免任務(wù)泄漏。以此為背景,我們認(rèn)為使用協(xié)程是在處理后臺(tái)任務(wù)和簡(jiǎn)化 Android 回調(diào)代碼的絕佳方案。

目前為止,我們主要集中在介紹協(xié)程是什么,以及如何管理它們,本文我們將介紹如何使用協(xié)程來(lái)完成一些實(shí)際任務(wù)。協(xié)程同函數(shù)一樣,是在編程語(yǔ)言特性中的一個(gè)常用特性,您可以使用它來(lái)實(shí)現(xiàn)任何可以通過(guò)函數(shù)和對(duì)象能實(shí)現(xiàn)的功能。但是,在實(shí)際編程中,始終存在兩種類(lèi)型的任務(wù)非常適合使用協(xié)程來(lái)解決:

  • 一次性請(qǐng)求 (one shot requests) 是那種調(diào)用一下就請(qǐng)求一下,請(qǐng)求獲取到結(jié)果后就結(jié)束執(zhí)行;
  • 流式請(qǐng)求 (streaming request) 在發(fā)出請(qǐng)求后,還一直監(jiān)聽(tīng)它的變化并返回給調(diào)用方,在拿到第一個(gè)結(jié)果之后它們也不會(huì)結(jié)束。

協(xié)程對(duì)于處理這些任務(wù)是一個(gè)絕佳的解決方案。在這篇文章中,我們將會(huì)深入介紹一次性請(qǐng)求,并探索如何在 Android 中使用協(xié)程實(shí)現(xiàn)它們。

一次性請(qǐng)求

一次性請(qǐng)求會(huì)調(diào)用一次就請(qǐng)求一次,獲取到結(jié)果后就結(jié)束執(zhí)行。這個(gè)模式同調(diào)用常規(guī)函數(shù)很像 —— 調(diào)用一次,執(zhí)行,然后返回。正因?yàn)橥瘮?shù)調(diào)用相似,所以相對(duì)于流式請(qǐng)求它更容易理解。

一次性請(qǐng)求會(huì)調(diào)用一次就請(qǐng)求一次,獲取到結(jié)果后就結(jié)束執(zhí)行。

舉例來(lái)說(shuō),您可以把它類(lèi)比為瀏覽器加載頁(yè)面。當(dāng)您點(diǎn)擊了這篇文章的鏈接后,瀏覽器向服務(wù)器發(fā)送了網(wǎng)絡(luò)請(qǐng)求,然后進(jìn)行頁(yè)面加載。一旦頁(yè)面數(shù)據(jù)傳輸?shù)綖g覽器后,瀏覽器就有了所有需要的數(shù)據(jù),然后停止同后端服務(wù)的對(duì)話。如果服務(wù)器后來(lái)又修改了這篇文章的內(nèi)容,新的更改是不會(huì)顯示在瀏覽器中的,除非您主動(dòng)刷新了瀏覽器頁(yè)面。

盡管這樣的方式缺少了流式請(qǐng)求那樣的實(shí)時(shí)推送特性,但是它還是非常有用的。在 Android 的應(yīng)用中您可以用這種方式解決很多問(wèn)題,比如對(duì)數(shù)據(jù)的查詢、存儲(chǔ)或更新,它還很適用于處理列表排序問(wèn)題。

問(wèn)題:展示一個(gè)有序列表

我們通過(guò)一個(gè)展示有序列表的例子來(lái)探索一下如何構(gòu)建一次性請(qǐng)求。為了讓例子更具體一些,我們來(lái)構(gòu)建一個(gè)用于商店員工使用的庫(kù)存應(yīng)用,使用它能夠根據(jù)上次進(jìn)貨的時(shí)間來(lái)查找相應(yīng)商品,并能夠以升序和降序的方式排列。因?yàn)檫@個(gè)倉(cāng)庫(kù)中存儲(chǔ)的商品很多,所以對(duì)它們進(jìn)行排序要花費(fèi)將近 1 秒鐘,因此我們需要使用協(xié)程來(lái)避免阻塞主線程。

在應(yīng)用中,所有的數(shù)據(jù)都會(huì)存儲(chǔ)到 Room 數(shù)據(jù)庫(kù)中。由于不涉及到網(wǎng)絡(luò)請(qǐng)求,因此我們不需要進(jìn)行網(wǎng)絡(luò)請(qǐng)求,從而專注于一次性請(qǐng)求這樣的編程模式。由于無(wú)需進(jìn)行網(wǎng)絡(luò)請(qǐng)求,這個(gè)例子會(huì)很簡(jiǎn)單,盡管如此它仍然展示了該使用怎樣的模式來(lái)實(shí)現(xiàn)一次性請(qǐng)求。

為了使用協(xié)程來(lái)實(shí)現(xiàn)此需求,您需要在協(xié)程中引入 ViewModel、Repository 和 Dao。讓我們逐個(gè)進(jìn)行介紹,看看如何把它們同協(xié)程整合在一起。

  1. class ProductsViewModel(val productsRepository: ProductsRepository): ViewModel() { 
  2.    private val _sortedProducts = MutableLiveData<List<ProductListing>>() 
  3.    val sortedProducts: LiveData<List<ProductListing>> = _sortedProducts 
  4.   
  5.    /** 
  6.     * 當(dāng)用戶點(diǎn)擊相應(yīng)排序按鈕后,UI 進(jìn)行調(diào)用 
  7.     */ 
  8.    fun onSortAscending() = sortPricesBy(ascending = true
  9.    fun onSortDescending() = sortPricesBy(ascending = false
  10.   
  11.    private fun sortPricesBy(ascending: Boolean) { 
  12.        viewModelScope.launch { 
  13.       // suspend 和 resume 使得這個(gè)數(shù)據(jù)庫(kù)請(qǐng)求是主線程安全的,所以 ViewModel 不需要關(guān)心線程安全問(wèn)題 
  14.            _sortedProducts.value = 
  15.                    productsRepository.loadSortedProducts(ascending) 
  16.        } 
  17.    } 

ProductsViewModel 負(fù)責(zé)從 UI 層接受事件,然后向 repository 請(qǐng)求更新的數(shù)據(jù)。它使用 LiveData 來(lái)存儲(chǔ)當(dāng)前排序的列表數(shù)據(jù),以供 UI 進(jìn)行展示。當(dāng)出現(xiàn)某個(gè)新事件時(shí),sortProductsBy 會(huì)啟動(dòng)一個(gè)新的協(xié)程對(duì)列表進(jìn)行排序,當(dāng)排序完成后更新 LiveData。在這種架構(gòu)下,通常都是使用 ViewModel 啟動(dòng)協(xié)程,因?yàn)檫@樣做的話可以在 onCleared 中取消所啟動(dòng)的協(xié)程。當(dāng)用戶離開(kāi)此界面后,這些任務(wù)就沒(méi)必要繼續(xù)進(jìn)行了。

  • LiveData:https://developer.android.google.cn/topic/libraries/architecture/livedata
  • ViewModel:https://developer.android.google.cn/topic/libraries/architecture/viewmodel

*如果您之前沒(méi)有用過(guò) LiveData,您可以看看這篇由 @CeruleanOtter 寫(xiě)的文章,它介紹了 LiveData 是如何為 UI 保存數(shù)據(jù)的 —— ViewModels: A Simple Example。

  • @CeruleanOtter:https://twitter.com/CeruleanOtter
  • ViewModels: A Simple Example:https://medium.com/androiddevelopers/viewmodels-a-simple-example-ed5ac416317e

這是在 Android 上使用協(xié)程的通用模式。由于 Android framework 不會(huì)主動(dòng)調(diào)用掛起函數(shù),所以您需要配合使用協(xié)程來(lái)響應(yīng) UI 事件。最簡(jiǎn)單的方法就是來(lái)一個(gè)事件就啟動(dòng)一個(gè)新的協(xié)程,最適合處理這種情況的地方就是 ViewModel 了。

在 ViewModel 中啟動(dòng)協(xié)程是很通用的模式。

ViewModel 實(shí)際上使用了 ProductsRepository 來(lái)獲取數(shù)據(jù),示例代碼如下:

  1. class ProductsRepository(val productsDao: ProductsDao) { 
  2.  
  3.   /** 
  4.        這是一個(gè)普通的掛起函數(shù),也就是說(shuō)調(diào)用方必須在一個(gè)協(xié)程中。repository 并不負(fù)責(zé)啟動(dòng)或者停止協(xié)程,因?yàn)樗⒉回?fù)責(zé)對(duì)協(xié)程生命周期的掌控。 
  5.        這可能會(huì)在 Dispatchers.Main 中調(diào)用,同樣它也是主線程安全的,因?yàn)?nbsp;Room 會(huì)為我們保證主線程安全。 
  6.     */ 
  7.    suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> { 
  8.        return if (ascending) { 
  9.            productsDao.loadProductsByDateStockedAscending() 
  10.        } else { 
  11.            productsDao.loadProductsByDateStockedDescending() 
  12.        } 
  13.    } 

ProductsRepository 提供了一個(gè)合理的同商品數(shù)據(jù)進(jìn)行交互的接口,此應(yīng)用中,所有內(nèi)容都存儲(chǔ)在本地 Room 數(shù)據(jù)庫(kù)中,它為 @Dao 提供了針對(duì)不同排序具有不同功能的兩個(gè)接口。

repository 是 Android 架構(gòu)組件中的一個(gè)可選部分,如果您在應(yīng)用中已經(jīng)集成了它或者其他的相似功能的模塊,那么它應(yīng)該更偏向于使用掛起函數(shù)。因?yàn)?repository 并沒(méi)有生命周期,它僅僅是一個(gè)對(duì)象,所以它不能處理資源的清理工作,所以默認(rèn)情況下,repository 中啟動(dòng)的所有協(xié)程都有可能出現(xiàn)泄漏。

使用掛起函數(shù)除了避免泄漏之外,在不同的上下文中也可以重復(fù)使用 repository,任何知道如何創(chuàng)建協(xié)程的都可以調(diào)用 loadSortedProducts,例如 WorkManager 所調(diào)度管理的后臺(tái)任務(wù)就可以直接調(diào)用它。

repository 應(yīng)該使用掛起函數(shù)來(lái)保證主線程安全。

注意: 當(dāng)用戶離開(kāi)界面后,有些在后臺(tái)中處理數(shù)據(jù)保存的操作可能還要繼續(xù)工作,這種情況下脫離了應(yīng)用生命周期來(lái)運(yùn)行是沒(méi)有意義的,所以大部分情況下 viewModelScope 都是一個(gè)好的選擇。

再來(lái)看看 ProductsDao,示例代碼如下:

  1. @Dao 
  2. interface ProductsDao { 
  3.  
  4.    // 因?yàn)檫@個(gè)方法被標(biāo)記為了 suspend,Room 將會(huì)在保證主線程安全的前提下使用自己的調(diào)度器來(lái)運(yùn)行這個(gè)查詢 
  5.    @Query("select * from ProductListing ORDER BY dateStocked ASC") 
  6.    suspend fun loadProductsByDateStockedAscending(): List<ProductListing> 
  7.    // 因?yàn)檫@個(gè)方法被標(biāo)記為了 suspend,Room 將會(huì)在保證主線程安全的前提下使用自己的調(diào)度器來(lái)運(yùn)行這個(gè)查詢 
  8.    @Query("select * from ProductListing ORDER BY dateStocked DESC") 
  9.    suspend fun loadProductsByDateStockedDescending(): List<ProductListing> 

ProductsDao 是一個(gè) Room @Dao,它對(duì)外提供了兩個(gè)掛起函數(shù),因?yàn)檫@些函數(shù)都增加了 suspend 修飾,所以 Room 會(huì)保證它們是主線程安全的,這也意味著您可以直接在 Dispatchers.Main 中調(diào)用它們。

*如果您沒(méi)有在 Room 中使用過(guò)協(xié)程,您可以先看看這篇由 @FMuntenescu 寫(xiě)的文章: Room 🔗 Coroutines

  • @FMuntenescu:https://twitter.com/FMuntenescu
  • Room 🔗 Coroutines:https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5

不過(guò)要注意的是,調(diào)用它的協(xié)程將會(huì)在主線程上執(zhí)行。所以,如果您要對(duì)執(zhí)行結(jié)果做一些比較耗時(shí)的操作,比如對(duì)列表內(nèi)容進(jìn)行轉(zhuǎn)換,您要確保這個(gè)操作不會(huì)阻塞主線程。

注意: Room 使用了自己的調(diào)度器在后臺(tái)線程上進(jìn)行查詢操作。您不應(yīng)該再使用 withContext(Dispatchers.IO) 來(lái)調(diào)用 Room 的 suspend 查詢,這只會(huì)讓您的代碼變復(fù)雜,也會(huì)拖慢查詢速度。

Room 的掛起函數(shù)是主線程安全的,并運(yùn)行于自定義的調(diào)度器中。

一次性請(qǐng)求模式

這是在 Android 架構(gòu)組件中使用協(xié)程進(jìn)行一次性請(qǐng)求的完整模式,我們將協(xié)程添加到了 ViewModel、Repository 和 Room 中,每一層都有著不同的責(zé)任分工。

  • ViewModel 在主線程上啟動(dòng)了協(xié)程,一旦有結(jié)果后就結(jié)束執(zhí)行;
  • Repository 提供了保證主線程安全的掛起函數(shù);
  • 數(shù)據(jù)庫(kù)和網(wǎng)絡(luò)層提供了保證主線程安全的掛起函數(shù)。

ViewModel 負(fù)責(zé)啟動(dòng)協(xié)程,并保證用戶離開(kāi)了相應(yīng)界面時(shí)它們就會(huì)被取消。它本身并不會(huì)做一些耗時(shí)的操作,而是依賴別的層級(jí)來(lái)做。一旦有了結(jié)果,就使用 LiveData 將數(shù)據(jù)發(fā)送到 UI 層。因?yàn)?ViewModel 并不做一些耗時(shí)操作,所以它是在主線程啟動(dòng)協(xié)程的,以便能夠更快地響應(yīng)用戶事件。

Repository 提供了掛起函數(shù)用來(lái)訪問(wèn)數(shù)據(jù),它通常不會(huì)啟動(dòng)一些生命周期比較長(zhǎng)的協(xié)程,因?yàn)樗鼈円坏﹩?dòng)了便無(wú)法取消。無(wú)論何時(shí) Repository 想要做一些耗時(shí)操作,比如對(duì)列表內(nèi)容進(jìn)行轉(zhuǎn)換,都應(yīng)該使用 withContext 來(lái)提供主線程安全的接口。

數(shù)據(jù)層 (網(wǎng)絡(luò)或數(shù)據(jù)庫(kù)) 總是會(huì)提供掛起函數(shù),使用 Kotlin 協(xié)程的時(shí)候要保證這些掛起函數(shù)是主線程安全的,Room 和 Retrofit 都遵循了這一點(diǎn)。

在一次性請(qǐng)求中,數(shù)據(jù)層只提供掛起函數(shù),調(diào)用方如果想要獲取最新的值,只能再次進(jìn)行調(diào)用,這就像瀏覽器中的刷新按鈕一樣。

花點(diǎn)時(shí)間讓您了解一次性請(qǐng)求的模式是值得,它在 Android 協(xié)程中是比較通用的模式,您會(huì)一直用到它。

第一個(gè) bug 出現(xiàn)了

在經(jīng)過(guò)測(cè)試后,您部署到了生產(chǎn)環(huán)境,運(yùn)行了幾周都感覺(jué)良好,直到您收到了一個(gè)很奇怪的 bug 報(bào)告:

標(biāo)題:🐞 — 排序錯(cuò)誤!

錯(cuò)誤報(bào)告: 當(dāng)我非常快速地點(diǎn)擊排序按鈕時(shí),排序的結(jié)果偶爾是錯(cuò)的,這還不是每次都能復(fù)現(xiàn)的🙃。

您研究了一下,不禁問(wèn)自己哪里出錯(cuò)了?這個(gè)邏輯很簡(jiǎn)單:

  • 開(kāi)始執(zhí)行用戶請(qǐng)求的排序操作;
  • 在 Room 調(diào)度器中開(kāi)始進(jìn)行排序;
  • 展示排序結(jié)果。

您覺(jué)得這個(gè) bug 不存在準(zhǔn)備關(guān)閉它,因?yàn)榻鉀Q方案很簡(jiǎn)單,"不要那么快地點(diǎn)擊按鈕",但是您還是很擔(dān)心,覺(jué)得還是哪個(gè)地方出了問(wèn)題。于是在代碼中加入一些日志,并跑了一堆測(cè)試用例后,您終于知道問(wèn)題出在什么地方了!

看起來(lái)應(yīng)用內(nèi)展示的排序結(jié)果并不是真正的 "排序結(jié)果",而是上一次完成排序的結(jié)果。當(dāng)用戶快速點(diǎn)擊按鈕時(shí),就會(huì)同時(shí)觸發(fā)多個(gè)排序操作,這些操作可能以任意順序結(jié)束。

當(dāng)啟動(dòng)一個(gè)新的協(xié)程來(lái)響應(yīng) UI 事件時(shí),要去考慮一下用戶若在上一個(gè)任務(wù)未完成之前又開(kāi)始了新的任務(wù),會(huì)有什么樣的后果。

這其實(shí)是一個(gè)并發(fā)導(dǎo)致的問(wèn)題,它和是否使用了協(xié)程其實(shí)沒(méi)有什么關(guān)系。如果您使用回調(diào)、Rx 或者是 ExecutorService,還是可能會(huì)遇到這樣的 bug。

有非常多方案能夠解決這個(gè)問(wèn)題,既可以在 ViewModel 中解決,又可以在 Repository 中解決。我們來(lái)看看怎么才能讓一次性請(qǐng)求按照我們所期望的順序返回結(jié)果。

最佳解決方案:禁用按鈕

核心問(wèn)題出在我們做了兩次排序,要修復(fù)的話我們可以只讓它排序一次。最簡(jiǎn)單的解決方法就是禁用按鈕,不讓它發(fā)出新的事件就可以了。

這看起來(lái)很簡(jiǎn)單,而且確實(shí)是個(gè)好辦法。實(shí)現(xiàn)起來(lái)的代碼也很簡(jiǎn)單,還容易測(cè)試,只要它能在 UI 中體現(xiàn)出來(lái)這個(gè)按鈕的狀態(tài),就完全可以解決問(wèn)題。

要禁用按鈕,只需要告訴 UI 在 sortPricesBy 中是否有正在處理的排序請(qǐng)求,示例代碼如下:

  1. // 方案 0: 當(dāng)有任何排序正在執(zhí)行時(shí),禁用排序按鈕 
  2.  
  3. class ProductsViewModel(val productsRepository: ProductsRepository): ViewModel() { 
  4.    private val _sortedProducts = MutableLiveData<List<ProductListing>>() 
  5.    val sortedProducts: LiveData<List<ProductListing>> = _sortedProducts 
  6.  
  7.    private val _sortButtonsEnabled = MutableLiveData<Boolean>() 
  8.    val sortButtonsEnabled: LiveData<Boolean> = _sortButtonsEnabled 
  9.  
  10.    init { 
  11.        _sortButtonsEnabled.value = true 
  12.    } 
  13.  
  14.    /** 
  15.        當(dāng)用戶點(diǎn)擊排序按鈕時(shí),調(diào)用 
  16.     */ 
  17.    fun onSortAscending() = sortPricesBy(ascending = true
  18.    fun onSortDescending() = sortPricesBy(ascending = false
  19.  
  20.    private fun sortPricesBy(ascending: Boolean) { 
  21.        viewModelScope.launch { 
  22.           // 只要有排序在進(jìn)行,禁用按鈕 
  23.            _sortButtonsEnabled.value = false 
  24.            try { 
  25.                _sortedProducts.value = 
  26.                        productsRepository.loadSortedProducts(ascending) 
  27.            } finally { 
  28.               // 排序結(jié)束后,啟用按鈕 
  29.                _sortButtonsEnabled.value = true 
  30.            } 
  31.        } 
  32.    } 

使用 sortPricesBy 中的 _sortButtonsEnabled 在排序時(shí)禁用按鈕

好了,這看起來(lái)還行,只需要在調(diào)用 repository 時(shí)在 sortPricesBy 內(nèi)部禁用按鈕就好了。

大部分情況下,這都是最佳解決方案,但是如果我們想在保持按鈕可用的前提下解決 bug 呢?這樣的話有一點(diǎn)困難,在本文剩余的部分看看該怎么做。

注意: 這段代碼展示了從主線程啟動(dòng)的巨大優(yōu)勢(shì),點(diǎn)擊之后按鈕立刻變得不可點(diǎn)了。但如果您換用了其他的調(diào)度程序,當(dāng)出現(xiàn)某個(gè)手速很快的用戶在運(yùn)行速度較慢的手機(jī)上操作時(shí),還是可能出現(xiàn)發(fā)送多次點(diǎn)擊事件的情況。

并發(fā)模式

下面幾個(gè)章節(jié)我們探討一些比較高級(jí)的話題,如果您才剛剛接觸協(xié)程,可以不去理解這一部分,使用禁用按鈕這一方案就是解決大部分類(lèi)似問(wèn)題的最佳方案。

在剩余部分我們將探索在不禁用按鈕的前提下,確保一次性請(qǐng)求能夠正常運(yùn)行。我們可以通過(guò)控制何時(shí)讓協(xié)程運(yùn)行 (或者不運(yùn)行) 來(lái)避免剛剛出現(xiàn)的并發(fā)問(wèn)題。

有三個(gè)基本的模式可以讓我們確保在同一時(shí)間只會(huì)有一次請(qǐng)求進(jìn)行:

  • 在啟動(dòng)更多協(xié)程之前取消之前的任務(wù);
  • 讓下一個(gè)任務(wù)排隊(duì)等待前一個(gè)任務(wù)執(zhí)行完成;
  • 如果有一個(gè)任務(wù)正在執(zhí)行,返回該任務(wù),而不是啟動(dòng)一個(gè)新的任務(wù)。

當(dāng)介紹完這三個(gè)方案后,您可能會(huì)發(fā)現(xiàn)它們的實(shí)現(xiàn)都挺復(fù)雜的。為了專注于設(shè)計(jì)模式而不是實(shí)現(xiàn)細(xì)節(jié),我創(chuàng)建了一個(gè) gist 來(lái)提供這三個(gè)模式的實(shí)現(xiàn)作為可重用抽象 。

方案 1:取消之前的任務(wù)

在排序這種情況下,獲取新的事件后就意味著可以取消上一個(gè)排序任務(wù)了。畢竟用戶通過(guò)這樣的行為已經(jīng)表明了他們不想要上次的排序結(jié)果了,繼續(xù)進(jìn)行上一次排序操作沒(méi)什么意義了。

要取消上一個(gè)請(qǐng)求,我們首先要以某種方式追蹤它。在 gist 中的 cancelPreviousThenRun 函數(shù)就做到了這個(gè)。

來(lái)看看如何使用它修復(fù)這個(gè) bug:

  1. // 方案 1: 取消之前的任務(wù) 
  2.   
  3. // 對(duì)于排序和過(guò)濾的情況,新請(qǐng)求進(jìn)來(lái),取消上一個(gè),這樣的方案是很適合的。 
  4.  
  5. class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) { 
  6.    var controlledRunner = ControlledRunner<List<ProductListing>>() 
  7.  
  8.    suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> { 
  9.       // 在開(kāi)啟新的排序之前,先取消上一個(gè)排序任務(wù) 
  10.        return controlledRunner.cancelPreviousThenRun { 
  11.            if (ascending) { 
  12.                productsDao.loadProductsByDateStockedAscending() 
  13.            } else { 
  14.                productsDao.loadProductsByDateStockedDescending() 
  15.            } 
  16.        } 
  17.    } 

使用 cancelPreviousThenRun 來(lái)確保同一時(shí)間只有一個(gè)排序任務(wù)在進(jìn)行

看一下 gist 中 cancelPreviousThenRun 中的代碼實(shí)現(xiàn),您可以學(xué)習(xí)到如何追蹤正在工作的任務(wù)。

  1. // see the complete implementation at 
  2. // 在 https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7 中查看完整實(shí)現(xiàn) 
  3. suspend fun cancelPreviousThenRun(block: suspend () -> T): T { 
  4.    // 如果這是一個(gè) activeTask,取消它,因?yàn)樗慕Y(jié)果已經(jīng)不需要了 
  5.    activeTask?.cancelAndJoin() 
  6.  
  7.    // ... 

簡(jiǎn)而言之,它會(huì)通過(guò)成員變量 activeTask 來(lái)保持對(duì)當(dāng)前排序的追蹤。無(wú)論何時(shí)開(kāi)始一個(gè)新的排序,都立即對(duì)當(dāng)前 activeTask 中的所有任務(wù)執(zhí)行 cancelAndJoin 操作。這樣會(huì)在開(kāi)啟一次新的排序之前就會(huì)把正在進(jìn)行中的排序任務(wù)給取消掉。

使用類(lèi)似于 ControlledRunner 這樣的抽象實(shí)現(xiàn)來(lái)對(duì)邏輯進(jìn)行封裝是比較好的方法,比直接混雜并發(fā)與應(yīng)用邏輯要好很多。

選擇使用抽象來(lái)封裝代碼邏輯,避免混雜并發(fā)和應(yīng)用邏輯代碼。

注意: 這個(gè)模式不適合在全局單例中使用,因?yàn)椴幌嚓P(guān)的調(diào)用方是不應(yīng)該相互取消。

  • gist:https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7#file-concurrencyhelpers-kt-L19
  • 代碼實(shí)現(xiàn):https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7#file-concurrencyhelpers-kt-L91
  • cancelAndJoin:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel-and-join.html

方案 2::讓下一個(gè)任務(wù)排隊(duì)等待

這里有一個(gè)對(duì)并發(fā)問(wèn)題總是有效的解決方案。

讓任務(wù)去排隊(duì)等待依次執(zhí)行,這樣同一時(shí)間就只會(huì)有一個(gè)任務(wù)會(huì)被處理。就像在商場(chǎng)里進(jìn)行排隊(duì),請(qǐng)求將會(huì)按照它們排隊(duì)的順序來(lái)依次處理。

對(duì)于這種特定的排序問(wèn)題,其實(shí)選擇方案 1 比使用本方案要更好一些,但還是值得介紹一下這個(gè)方法,因?yàn)樗偸悄軌蛴行У慕鉀Q并發(fā)問(wèn)題。

  1. // 方案 2: 使用互斥鎖 
  2. // 注意: 這個(gè)方法對(duì)于排序或者是過(guò)濾來(lái)說(shuō)并不是一個(gè)很好的解決方案,但是它對(duì)于解決網(wǎng)絡(luò)請(qǐng)求引起的并發(fā)問(wèn)題非常適合。 
  3.  
  4. class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) { 
  5.    val singleRunner = SingleRunner() 
  6.  
  7.    suspend fun loadSortedProducts(ascending: Boolean): List<ProductListing> { 
  8.       // 開(kāi)始新的任務(wù)之前,等待之前的排序任務(wù)完成 
  9.        return singleRunner.afterPrevious { 
  10.            if (ascending) { 
  11.                productsDao.loadProductsByDateStockedAscending() 
  12.            } else { 
  13.                productsDao.loadProductsByDateStockedDescending() 
  14.            } 
  15.        } 
  16.    } 

無(wú)論何時(shí)進(jìn)行一次新的排序, 都使用一個(gè) SingleRunner 實(shí)例來(lái)確保同時(shí)只會(huì)有一個(gè)排序任務(wù)在進(jìn)行。

它使用了 Mutex,可以把它理解為一張單程票 (或是鎖),協(xié)程在必須要獲取鎖才能進(jìn)入代碼塊。如果一個(gè)協(xié)程在運(yùn)行時(shí),另一個(gè)協(xié)程嘗試進(jìn)入該代碼塊就必須掛起自己,直到所有的持有 Mutex 的協(xié)程完成任務(wù),并釋放 Mutex 后才能進(jìn)入。

Mutex 保證同時(shí)只會(huì)有一個(gè)協(xié)程運(yùn)行,并且會(huì)按照啟動(dòng)的順序依次結(jié)束。

  • Mutex:https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7#file-concurrencyhelpers-kt-L49

方案 3:復(fù)用前一個(gè)任務(wù)

第三種可以考慮的方案是復(fù)用前一個(gè)任務(wù),也就是說(shuō)新的請(qǐng)求可以重復(fù)使用之前存在的任務(wù),比如前一個(gè)任務(wù)已經(jīng)完成了一半進(jìn)來(lái)了一個(gè)新的請(qǐng)求,那么這個(gè)請(qǐng)求直接重用這個(gè)已經(jīng)完成了一半的任務(wù),就省事很多。

但其實(shí)這種方法對(duì)于排序來(lái)說(shuō)并沒(méi)有多大意義,但是如果是一個(gè)網(wǎng)絡(luò)數(shù)據(jù)請(qǐng)求的話,就很適用了。

對(duì)于我們的庫(kù)存應(yīng)用來(lái)說(shuō),用戶需要一種方式來(lái)從服務(wù)器獲取最新的商品庫(kù)存數(shù)據(jù)。我們提供了一個(gè)刷新按鈕這樣的簡(jiǎn)單操作來(lái)讓用戶點(diǎn)擊一次就可以發(fā)起一次新的網(wǎng)絡(luò)請(qǐng)求。

當(dāng)請(qǐng)求正在進(jìn)行時(shí),禁用按鈕就可以簡(jiǎn)單地解決問(wèn)題。但是如果我們不想這樣,或者說(shuō)不能這樣,我們就可以選擇這種方法復(fù)用已經(jīng)存在的請(qǐng)求。

查看下面的來(lái)自 gist 的使用了 joinPreviousOrRun 的示例代碼:

  1. class ProductsRepository(val productsDao: ProductsDao, val productsApi: ProductsService) { 
  2.    var controlledRunner = ControlledRunner<List<ProductListing>>() 
  3.  
  4.    suspend fun fetchProductsFromBackend(): List<ProductListing> { 
  5.       // 如果已經(jīng)有一個(gè)正在運(yùn)行的請(qǐng)求,那么就返回它。如果沒(méi)有的話,開(kāi)啟一個(gè)新的請(qǐng)求。 
  6.        return controlledRunner.joinPreviousOrRun { 
  7.            val result = productsApi.getProducts() 
  8.            productsDao.insertAll(result) 
  9.            result 
  10.        } 
  11.    } 

上面的代碼行為同 cancelPreviousAndRun 相反,它會(huì)直接使用之前的請(qǐng)求而放棄新的請(qǐng)求,而 cancelPreviousAndRun 則會(huì)放棄之前的請(qǐng)求而創(chuàng)建一個(gè)新的請(qǐng)求。如果已經(jīng)存在了正在運(yùn)行的請(qǐng)求,它會(huì)等待這個(gè)請(qǐng)求執(zhí)行完成,并將結(jié)果直接返回。只有不存在正在運(yùn)行的請(qǐng)求時(shí)才會(huì)創(chuàng)建新的請(qǐng)求來(lái)執(zhí)行代碼塊。

您可以在 joinPreviousOrRun 開(kāi)始時(shí)看到它是如何工作的,如果 activeTask 中存在任何正在工作的任務(wù),就直接返回它。

  1. // 在 https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7#file-concurrencyhelpers-kt-L124 中查看完整實(shí)現(xiàn) 
  2.  
  3. suspend fun joinPreviousOrRun(block: suspend () -> T): T { 
  4.    // 如果存在 activeTask,直接返回它的結(jié)果,并不會(huì)執(zhí)行代碼塊 
  5.     activeTask?.let { 
  6.         return it.await() 
  7.     } 
  8.     // ... 

這個(gè)模式很適合那種通過(guò) id 來(lái)查詢商品數(shù)據(jù)的請(qǐng)求。您可以使用 map 來(lái)建立 id 到 Deferred 的映射關(guān)系,然后使用相同的邏輯來(lái)追蹤同一個(gè)產(chǎn)品之前的請(qǐng)求數(shù)據(jù)。

直接復(fù)用之前的任務(wù)可以有效避免重復(fù)的網(wǎng)絡(luò)請(qǐng)求。

  • joinPreviousOrRun:https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7#file-concurrencyhelpers-kt-L124

下一步

在這篇文章中,我們探討了如何使用 Kotlin 協(xié)程來(lái)實(shí)現(xiàn)一次性請(qǐng)求。我們實(shí)現(xiàn)了如何在 ViewModel 中啟動(dòng)協(xié)程,然后在 Repository 和 Room Dao 中提供公開(kāi)的 suspend function,這樣形成了一個(gè)完整的編程范式。

對(duì)于大部分任務(wù)來(lái)說(shuō),在 Android 上使用 Kotlin 協(xié)程按照上面這些方法就已經(jīng)足夠了。這些方法就像上面所說(shuō)的排序一樣可以應(yīng)用在很多場(chǎng)景中,您也可以使用這些方法來(lái)解決查詢、保存、更新網(wǎng)絡(luò)數(shù)據(jù)等問(wèn)題。

然后我們探討了一下可能出現(xiàn) bug 的地方,并給出了解決方案。最簡(jiǎn)單 (往往也是最好的) 的方案就是從 UI 上直接更改,排序運(yùn)行時(shí)直接禁用按鈕。

最后,我們探討了一些高級(jí)并發(fā)模式,并介紹了如何在 Kotlin 協(xié)程中實(shí)現(xiàn)它們。雖然這些代碼有點(diǎn)復(fù)雜,但是為一些高級(jí)協(xié)程方面的話題做了很好的介紹。

【本文是51CTO專欄機(jī)構(gòu)“谷歌開(kāi)發(fā)者”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者(微信公眾號(hào):Google_Developers)】

戳這里,看該作者更多好文

 

責(zé)任編輯:趙寧寧 來(lái)源: 51CTO專欄
相關(guān)推薦

2020-04-08 09:06:34

Android 協(xié)程開(kāi)發(fā)

2020-04-23 09:33:32

Android 協(xié)程開(kāi)發(fā)

2019-10-23 14:34:15

KotlinAndroid協(xié)程

2025-06-26 04:10:00

2023-10-24 19:37:34

協(xié)程Java

2021-08-04 16:19:55

AndroidKotin協(xié)程Coroutines

2023-12-22 09:11:45

AndroidNFC移動(dòng)開(kāi)發(fā)

2021-09-16 09:59:13

PythonJavaScript代碼

2025-03-26 01:22:00

NtyCo協(xié)程框架

2018-03-26 14:25:55

KubernetesSkaffold命令

2012-04-19 12:58:26

TitaniumJSS

2010-10-18 13:16:24

GalleryAndroid

2023-11-17 11:36:59

協(xié)程纖程操作系統(tǒng)

2020-04-07 11:10:30

Python數(shù)據(jù)線程

2019-03-01 08:57:47

iOScoobjc協(xié)程

2025-02-08 09:13:40

2021-12-09 06:41:56

Python協(xié)程多并發(fā)

2025-02-28 09:04:08

2009-07-16 14:22:02

Windows Emb

2009-06-05 15:04:36

Eclipse代碼模版
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

国外成人在线播放| 欧美视频免费在线| 99精品99久久久久久宅男| 国产67194| 国产伦精品一区二区三区免费优势| 亚洲免费资源在线播放| 成人自拍视频网站| 成人免费视频国产免费| 欧美福利电影在线观看| 亚洲欧美中文日韩在线| 精品人妻一区二区三区免费| 亚洲欧美韩国| 一区二区三区日韩| 视频一区二区在线| 亚洲国产综合网| 噜噜噜久久亚洲精品国产品小说| 自拍偷拍亚洲欧美| 亚洲欧美在线不卡| 久久久91麻豆精品国产一区| 日本高清不卡aⅴ免费网站| 欧美在线观看视频免费| 电影在线高清| 99久久er热在这里只有精品15 | gogo高清在线播放免费| 国产欧美一区二区三区沐欲| 国产精品二区三区四区| 国产一区二区三区在线观看| 亚洲一区激情| 欧美精品videosex极品1| av资源在线免费观看| 一道本一区二区三区| 亚洲爱爱爱爱爱| 日韩a一级欧美一级| 高清在线一区| 欧美视频在线不卡| 日韩av一二三四| а√天堂中文在线资源8| 亚洲丝袜精品丝袜在线| 亚洲一区二区精品在线观看| 精品亚洲综合| 久久这里都是精品| 麻豆av一区二区三区| 色婷婷激情五月| 成人免费av在线| 国产精品视频福利| 人妻91麻豆一区二区三区| 国产成人在线视频网站| 亚洲字幕在线观看| hs视频在线观看| 国产精品77777| 亚洲xxxx做受欧美| 成人乱码一区二区三区| 高清国产一区二区| 国产经典一区二区三区| 成人毛片在线免费观看| 成人午夜碰碰视频| 精品国产乱码久久久久| 色综合免费视频| 99精品桃花视频在线观看| 久久久久久高清| 极品白浆推特女神在线观看| 欧美激情一区二区三区在线| 日韩电影大全在线观看| 国产在线一在线二| 中文字幕制服丝袜一区二区三区| 亚洲欧美日韩精品久久久 | 欧美福利在线播放网址导航| 亚洲国模精品私拍| 蜜桃无码一区二区三区| 成人中文在线| 久久香蕉国产线看观看av| 亚洲一级生活片| 欧美日韩国产成人精品| 国内精品久久久久影院优 | 亚洲综合欧美| 国产精品日韩在线| a级片在线免费看| 91丨九色丨蝌蚪丨老版| 色综合视频二区偷拍在线| 欧美成年黄网站色视频| 一区二区三区 在线观看视频| 18禁裸男晨勃露j毛免费观看| 中文在线а√天堂| 欧美人妖巨大在线| 亚洲一区和二区| 神马影视一区二区| 欧美xxxx综合视频| 国产精品免费精品一区| 久久99国产精品麻豆| 高清国语自产拍免费一区二区三区| 三级小视频在线观看| 欧美国产精品一区| 青青青青在线视频| 草民电影神马电影一区二区| 欧美不卡一区二区| 亚洲精品国产精品国自产网站| 香蕉av一区二区| 8x拔播拔播x8国产精品| 亚洲一区二区影视| 91亚洲精品久久久蜜桃| 一卡二卡3卡四卡高清精品视频| 香蕉久久aⅴ一区二区三区| 色婷婷久久综合| 在线观看一区二区三区视频| 深爱激情综合| 国精产品一区一区三区有限在线| 啪啪小视频网站| 成人动漫视频在线| 在线视频一区观看| 午夜精品成人av| 精品国产乱码久久久久久免费| 最近中文字幕在线mv视频在线| 欧美成人国产| 国产日韩在线看| 国产在线播放av| 亚洲超碰97人人做人人爱| 日韩在线一区视频| 国产精品欧美三级在线观看| 欧美激情一区二区三区久久久 | 欧美不卡1区2区3区| 亚洲七七久久综合桃花剧情介绍| 色悠悠久久综合| 中文字幕人妻一区| 综合一区av| 91精品一区二区| 成人高潮成人免费观看| 欧美视频在线免费| 成年人的黄色片| 国产一区亚洲| 99精彩视频| 182tv在线播放| 欧美一区二区三区的| 黄色片在线观看免费| 先锋影音国产一区| 久久久水蜜桃| 天堂中文最新版在线中文| 精品粉嫩aⅴ一区二区三区四区| 色欲人妻综合网| 精品一区二区三区在线播放视频 | 亚洲伊人久久大香线蕉av| 北条麻妃在线| 欧美三电影在线| 亚洲а∨天堂久久精品2021| 日韩一区精品字幕| 日本一区视频在线播放| 欧美韩国亚洲| 在线亚洲欧美视频| 一级全黄裸体免费视频| 久久久国产一区二区三区四区小说 | 色域天天综合网| 中文字幕一区二区人妻在线不卡| 亚洲免费精品| 九九久久99| 成人爱爱网址| 亚洲色图50p| 黄色大全在线观看| 国产精品久久午夜| 国产在线观看中文字幕| 欧美一区二区三区久久精品| 91精品免费| 国产深夜视频在线观看| 亚洲国产精品成人精品| 成人免费区一区二区三区| 26uuu欧美| 日本xxxxxxx免费视频| 欧美一二区在线观看| 国产精选久久久久久| 里番在线观看网站| 日韩欧美一卡二卡| 国产成人精品亚洲男人的天堂| 99麻豆久久久国产精品免费优播| 黄色片视频在线免费观看| 欧美日韩爱爱| 91中文精品字幕在线视频| 神马午夜伦理不卡| 日韩精品在线私人| 亚洲天堂网视频| 一区二区三区欧美久久| 亚洲乱码国产乱码精品精大量| 日韩高清在线一区| 大桥未久一区二区| 国产精品欧美大片| 国产精品大陆在线观看| bt在线麻豆视频| 日韩精品www| 91国产精品一区| 亚瑟在线精品视频| 欧美丰满老妇熟乱xxxxyyy| 国产自产视频一区二区三区| 91精品国产91久久久久麻豆 主演| 妖精视频一区二区三区免费观看| 成人国产精品色哟哟| 国产精品vvv| 久久精品免费播放| 污污的视频网站在线观看| 欧美日韩一区三区| 国产真实的和子乱拍在线观看| 国产亚洲一区字幕| 国产日本亚洲高清| 欧美精品 - 色网| 亚洲黄色影院| 一区二区高清视频| 天天久久夜夜| 91在线色戒在线| 欧美极度另类| 欧美激情亚洲一区| avav免费在线观看| 欧美精品一区二区三| 亚洲熟女乱色一区二区三区久久久| 亚洲成人激情综合网| 萌白酱视频在线| 久久久精品免费免费| 亚洲欧美日韩中文字幕在线观看| 日韩高清国产一区在线| 欧美精品久久久久久久自慰| 欧美成人milf| 日本免费高清一区| 嗯用力啊快一点好舒服小柔久久| 国产精品一二三视频| 欧美伦理91| 久久亚洲国产精品成人av秋霞| 欧美拍拍视频| 亚洲成人国产精品| 国产三级漂亮女教师| 欧美亚洲国产一区二区三区va | 麻豆av在线导航| 亚洲一区999| 日本a一级在线免费播放| 精品乱人伦一区二区三区| 国产精品久久久久久久久久久久久久久久 | 欧美一区二区三区免费视 | 日本精品性网站在线观看| 污污的视频在线观看| 久久天堂av综合合色| 91在线视频| 一区二区欧美久久| 国产露出视频在线观看| 亚洲视频日韩精品| 久草在线青青草| 亚洲色图17p| 丁香在线视频| 中文字幕日韩在线视频| 黄上黄在线观看| 在线视频欧美性高潮| 国产三级在线看| 国产亚洲精品久久久久久| 美女毛片在线看| 国产亚洲a∨片在线观看| 蜜芽tv福利在线视频| 国产亚洲精品综合一区91| 国产一级在线| 在线免费看av不卡| 中文字幕日本在线| 日韩一区av在线| 国产激情在线观看| 欧美乱大交xxxxx| 伦理av在线| 久久久亚洲精选| 欧美日韩国产观看视频| 91精品国产91久久久| 欧美日韩大片| 国产精品人成电影在线观看| 老司机精品视频网| 亚洲一区二区在线播放| 2020最新国产精品| 久久99欧美| 精品美女久久久| 青青草原国产免费| 欧美三级黄美女| 91九色在线观看视频| 天堂一区二区在线免费观看| 污污动漫在线观看| 国产成人一级电影| 特级西西人体wwwww| 国产亚洲精品aa午夜观看| 自拍偷拍第9页| 亚洲主播在线播放| 久久久精品免费看| 欧美日韩一区二区在线视频| 国产精品人人爽| 亚洲精品美女在线观看播放| 免费福利在线观看| 欧美成年人在线观看| 2022成人影院| 国产原创欧美精品| 欧美自拍一区| 亚洲看片网站| 亚洲精选在线| 在线观看免费污视频| 国产91丝袜在线观看| 熟女俱乐部一区二区| 亚洲欧洲综合另类在线| 97超碰人人干| 91精品午夜视频| 香蕉国产在线视频| 久久精品国产久精国产思思| av电影院在线看| 国产美女精品视频免费观看| 91午夜精品| 在线亚洲美日韩| 免费日韩精品中文字幕视频在线| 中文字幕亚洲欧洲| 久久综合久久综合久久综合| 北条麻妃在线观看视频| 欧美午夜激情视频| 性生交大片免费看女人按摩| 亚洲欧美日韩中文在线制服| 污视频在线免费观看网站| 国产精品久久久久久久久粉嫩av | 久久成人精品无人区| 久久人妻少妇嫩草av无码专区| 国产精品福利一区二区三区| 亚洲国产成人无码av在线| 欧美成人国产一区二区| 男人在线资源站| 日本高清久久天堂| 国内精品国产成人国产三级粉色| 亚洲一区免费看| 石原莉奈一区二区三区在线观看 | 欧美韩日一区二区三区四区| 国产精品16p| 欧美高清视频一二三区| 国产小视频免费在线观看| 97精品国产91久久久久久| 奇米一区二区| 中文字幕制服丝袜在线| 另类中文字幕网| 影音先锋男人在线| 色94色欧美sute亚洲线路二 | 成人午夜免费电影| 免费三片在线播放| 日韩一区二区精品葵司在线| 激情视频在线观看| 国产精品永久免费视频| 精品国产视频| 狠狠97人人婷婷五月| 99精品久久只有精品| 免费观看一级视频| 欧美xfplay| 三级福利片在线观看| 99爱精品视频| 中文字幕亚洲综合久久五月天色无吗'' | 久久久久久久黄色| 日韩精品一区二| 色呦呦呦在线观看| 国产不卡一区二区在线观看| 午夜精品亚洲| 国产精九九网站漫画| 亚洲成人激情av| 欧美精品少妇| 国产精品大陆在线观看| 青青草国产成人a∨下载安卓| 北条麻妃av高潮尖叫在线观看| 久久久不卡影院| 黄色污污视频软件| 最近中文字幕2019免费| 国产精品成人国产| 中文字幕第一页亚洲| 国产乱人伦偷精品视频不卡| 国产精品免费人成网站酒店| 日韩免费看网站| 51av在线| 欧美一区二区三区四区在线观看地址| 久热re这里精品视频在线6| 国产又黄又粗视频| 欧美军同video69gay| 在线黄色网页| 久久久婷婷一区二区三区不卡| 日韩制服丝袜av| 最新一区二区三区| 亚洲第一偷拍网| 精品亚洲美女网站| 最新国产精品久久| 成人性生交大片免费看中文| 久久久国产精品成人免费| 中文欧美在线视频| 亚洲网址在线观看| 欧美啪啪免费视频| 国产精品视频免费| 精品国精品国产自在久不卡| 午夜欧美大片免费观看| 成人看的羞羞网站| 亚洲熟女乱综合一区二区| 一本色道久久综合亚洲aⅴ蜜桃| 137大胆人体在线观看| 成人一区二区三区四区| 久久国产一二区| 波多野结衣亚洲一区二区| 日韩精品在线视频观看| 91精品一区| 干日本少妇首页| 亚洲天堂av一区| 九色在线免费| 国产成人一区二区三区免费看| 久久亚洲国产精品一区二区| 亚洲av熟女国产一区二区性色| 精品国产区一区| 韩国精品视频在线观看| 精品少妇在线视频| 成人欧美一区二区三区白人|