背景
在app開發過程中,實現polling邏輯也是很常見的。當然在移動端應用使用polling處理會影響應用的性能。比如polling處理增加了網絡請求的次數,服務端壓力增加。polling處理也消耗了更多的網絡流量。但是應用polling的場景還是有的。有時是否選擇polling要考慮很多綜合的因素,比如我們可以使用長連接替代polling,但是長連接在服務端和客户端的開發成本相對要更高些,如果polling只是實現類似的跟帖等功能,我們完全可以使用polling實現,而不是選擇代價更高的長連接方案。下面會分使用flow和不使用flow兩種方式實現polling並對比兩種方式的優缺點。
不使用flow
我們使用線程處理polling請求,首先我們定義了一個polling thread。
class PollingThread: Thread() {
override fun run() {
var successBlock : (PollingData)->Unit = {
Log.d("PollingThread","successBlock $it")
}
var failBlock:(Exception)->Unit ={
Log.d("PollingThread","failBlock $it")
}
while (isInterrupted) {
pollingApi.call(successBlock, failBlock)
Thread.sleep(5000)
}
}
}
在run方法中實現了polling接口的調用,並且接口的調用在while循環中。這裏假設polling的時間間隔是5秒鐘,所以這裏調用線程的sleep方法暫停線程的執行,5秒後再次調用polling接口。polling接口的調用是異步過程,所以這裏設置了兩個回調,一個用於接收成功的數據,一個用於接收失敗的異常。如果在回調中更新了畫面,我們還要考慮如何保證回調在ui線程執行,並且回調中不更新消失的頁面元素。
class PollingThread(val lifecycleOwner: LifecycleOwner): Thread() {
override fun run() {
var successBlock : (PollingData)->Unit = {
Handler(Looper.getMainLooper()).post {
if(lifecycleOwner.lifecycle.currentState >= Lifecycle.State.RESUMED) {
Log.d("PollingThread", "successBlock $it")
}
}
}
var failBlock:(Exception)->Unit ={
Handler(Looper.getMainLooper()).post {
if(lifecycleOwner.lifecycle.currentState >= Lifecycle.State.RESUMED) {
Log.d("PollingThread", "failBlock $it")
}
}
}
while (isInterrupted) {
pollingApi.call(successBlock, failBlock)
Thread.sleep(5000)
}
}
}
這段代碼增加了回調的線程切換和ui畫面有效判斷。使用Handler切換線程到ui線程,lifecycler判斷ui畫面的有效性。
polling線程已經定義完成,下一步我們還要在適當的時機啓動polling線程和停止polling線程。
var pollingThread:PollingThread? = null
override fun onResume() {
super.onResume()
pollingThread = PollingThread().run {
start()
this
}
}
override fun onPause() {
super.onPause()
pollingThread?.interrupt()
pollingThread = null
}
這裏定義了一個變量pollingThread用於保存啓動的polling線程,我們在onResume方法中啓動polling線程,在onPause方法中停止線程。經過這樣處理後polling就可以工作了。
使用flow
首先我們需要定義一個polling flow。
private val pollingFlow = flow {
while (true) {
emit(serverApi.getPollingData())
delay(2000)
}
}
在flow中使用了while循環實現無限輪訓,請求的網絡接口被定義成了掛起函數,輪訓間隔通過協程的delay方法實現。對比不使用flow的方式,polling flow 有自己的一些優勢。①無線輪訓控制更加簡單,不需要複雜邏輯判斷,因為flow 中的輪訓邏輯中有掛起函數的調用,當收集polling flow的協程被取消時,掛起函數會拋出取消異常,這樣就達到了輪訓邏輯控制的目的了。②由於調用服務器的接口函數是掛起函數,所以這裏避免了使用callback 方法。
我們如何控制線程切換,如何輪訓異常呢?
private val pollingFlow = flow {
while (true) {
emit(serverApi.getPollingData())
delay(5000)
}
}.flowOn(Dispatchers.IO).retryWhen { cause, attempt ->
Log.d("polling flow ", "retryWhen cause $cause attempt $attempt")
delay(5000)
true
}.onEach {
Log.d("polling flow ", "onEach $it")
}
lifecycleScope.launchWhenResumed { pollingFlow.collect() }
我們可以通過flowOn方法切換線程,保證了輪訓執行的線程在io線程。在polling flow收集的時候使用默認的ui線程。這樣保證了flowOn方法前的部分執行在io線程,flowOn方法後的部分執行在ui線程,進而達到線程切換的目的。這裏使用retryWhen方法處理輪訓異常,當有異常發生時,延時polling時間間隔後進行重試。
我們調用了lifecycleScope.launchWhenResumed方法收集flow,這樣保證了polling flow只在畫面被喚醒的狀態下被收集。launchWhenResumed方法是通過切斷消息分發來達到掛起的目的,如果在launchWhenResumed方法中又啓動了協程進行輪訓操作,那麼阻止消息分發並不能停止launchWhenResumed方法內部啓動協程的輪訓操作。在lifecycle-runtime-ktx 2.4.0版本中引入了lifecycle.repeatOnLifecycle方法,這個方法可以根據生命週期進行取消和重啓。由於它實現的是協程取消和協程重啓,所以在這個方法內部啓動的協程也會被取消和重啓,進而解決了畫面掛起時子協程不被取消而引起的泄露問題。
總結
flow可以充分利用協程的結構化異步的優勢實現異步輪訓,避免使用啓動線程方式進行輪訓操作。
線程輪訓的方式中延時操作阻塞了線程,flow中的延時操作掛起協程但不阻塞線程,所以flow節省了線程資源,協程掛起時線程還可以處理其他的任務。
創建線程的代價比啓動協程的代價更高,並且線程的管理更加麻煩,我們要時刻關心線程狀態,控制線程的啓動與停止。但是協程依仗結構化異步的特點,用户不需要投入過多的經歷管理協程的啓動和停止。
使用flow可以通過聲明的方式定義polling處理流程,代碼邏輯簡單清晰。比如通過flow的retryWhen聲明重試處理,通過catch捕獲polling異常,通過flowOn方法進行線程切換等。
我的公眾號已經開通,公眾號會同步發佈。
歡迎關注我的公眾號
————————————————
版權聲明:本文為CSDN博主「mjlong123123」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/mjlong1...