天天看點

android java協程,Android協程——入門

一、如何使用協程

1.1 添加依賴

implementation

'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'

implementation

'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'

1.2 使用協程Coroutine

在kotlinx.coroutines包中,你可以使用launch或async啟動一個協程。從概念上講,async就像launch一樣,它啟動一個單獨的協程,協程相當于一個輕量級的線程,與其他所有的協同程式同時工作。

async和launch不同的地方在于,launch傳回一個Job并且不攜帶任何結果值,而async傳回Deffered。

Deffered表示一個輕量級的非阻塞未來,表示稍後提供結果的承諾。我們可以使用await()方法擷取一個deffered的傳回結果。Deffered本質上也是Job,是以可以在需要的時候取消它。

如果在launch中的代碼因為異常而終止,那麼它會被是為線程中未捕獲異常而導緻應用崩潰。異步代碼中未捕獲異常存儲在生成的Deffered中,并且不會在其他任何地方傳遞,除非經過處理,否則它會被靜默删除。

協程分發

在Android中,我們常用的又兩個分發器dispatcher:

uiDispatcher:将執行分發到Android主UI線程(用于父協程)

bgDispatcher:在背景線程中排程執行(用于子協程)

// dispatches execution into Android main thread

val uiDispatcher: CoroutineDispatcher = Dispatcher.Main

// represent a pool of shared thread as coroutine dispatcher

val bgDispatcher: CoroutineDispatcher = Dispatcher.IO

協程作用域

使用協程需要提供協程對應的作用域CoroutineScope或使用GlobalScope

// GlobalScope示例

class MainFragment : Fragment(){

fun loadData() = GlobalScope.launch{...}

}

//CoroutineScope示例

class MainFragment : Fragment(){

val uiScope = CoroutineScope(Dispatchers.Main)

fun loadData() = uiScope.launch{...}

}

//Fragment實作CoroutineScope示例

class MainFragment : Fragment(),CoroutineScope{

override val coroutineContext: CoroutineContext

get() = Dispatcher.Main

fun loadData() = launch {...}

}

lauch+async(執行任務)

父協程通過Main Dispatcher調用的launch方法啟動。

子協程通過IO Dispatcher調用async方法啟動。

Note:父協程會一直等待它的子協程完成

Note:協程如果發生未捕獲異常,程式會崩潰

val uiScope = CoroutineScope(Dispatchers.Main)

fun loadData() = uiScope.launch {

view.showLoading() //ui thread

val task = async(bgDispatcher){ //background thread

// your blocking call

}

val result = task.await()

view.showDta()

}

lauch+withContext(執行任務)

使用上一個例子中的方法,我們可以正常的運作。但我們浪費了啟用第二個背景任務協程的資源。

如果我們隻啟用一個協程,可以使用withContext來優化我們的代碼。

背景任務通過帶有IO Dispatcher的withContext函數執行。

val uiScope = CoroutineScope(Dispatcher.Main)

fun loadData() = uiScope.launch {

view.showLoading() //ui thread

val result = withContext(bgDispatcher){

// your blocking call

}

view.showData(result) // ui thread

}

launch+ withContext(按順序執行兩個任務)

val uiScope = CoroutineScope(Dispatchers.Main)

fun loadData() = uiScope.launch {

view.showLoading() // ui thread

val result1 = withContext(bgDispatcher){

// your blocking call

}

val result2 = withContext(bgDispatcher){

//your blocking call

}

val result = result1 + result2

vuew,showData(result) //ui thread

}

launch+async+async(并行執行兩個任務)

val uiScope = CoroutineScope(Dispatcher.Main)

fun loadData() = uiScope.launch {

view.showLoading() // ui thread

val task1 = async(bgDispatcher){

//your blocking call

}

val task2 = async(bgDispatcher){

//your blocking call

}

val result = task1.await() + task2.await()

view.showData() // ui thread

}

二、如何使用協程的timeout

如果我們想為一個協程任務設定逾時,我們可以使用withTimeoutOrNull()方法,如果逾時就傳回null。

val uiScope = CoroutineScope(Dispatchers.Main)

fun loadData() = uiScope.launch {

view.showLoading() // ui thread

val task = async(bgDispatcher){

//your blocking call

}

// suspend until task is finished or return null in 2s

val result = withTimeoutOrNull(2000) { task.await() }

view.showData(result) // ui thread

}

三、如何取消一個協程

3.1 job

loadData()方法傳回一個Job對象,Job對象是可以被取消的。當父協程被取消的時候,它的所有子協程都會被結束。當stopPresenting()方法被調用,view.showData()肯定不會被調用。

val uiScope = CoroutineScope(Dispatchers.Main)

val job: Job? = null

fun startPresenting(){

job = loadData()

}

fun stopPresenting(){

job?.cancel()

}

fun loadData() = uiScope.launch {

view.showLoading() // ui thread

val result = withContext(bgDispatcher){

// your blocking call

}

view.showData(result) //ui thread

}

3.2 parent job

取消協程的另一種方法是建立SupervisorJob對象,并通過重載+運算符在作用域構造函數中指定它。

var job = SipervisorJob()

val uiScope = CoroutineScope(Dispatchers.Main + job)

fun startPresenting(){

loadData()

}

fun stopPresenting(){

scope.coroutineContext.cancelChildren()

}

fun loadData() = uiScope.launch {

view.showLoading()

val result = withContext(bgDispatcher) {

// your blocking call

}

view.showData(result)

}

3.3 自定義具有生命周期感覺的協程作用域

class MainScope : CoroutineScope, LifecycleObsever {

private val job = SupervisorJob()

override val coroutineContext: CoroutineContext

get() = job + Dispatchers.Main

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)

fun destory() = coroutineContext.cancelChildren()

}

//使用

class MainFragment : Fragment(){

private val uiScope = MainScope()

override fun onCreate(savedInstanceState: Bundle?){

super.onCreate(savedInstanceState)

lifecycle.addObserver(mainScope)

}

private fun loadData() = uiScope.launch {

val result = withContext(bgDispatcher) {

// your blocking call

}

}

}

下面,舉一個在ViewModel中使用生命周期感覺的協程。

open class ScopedViewModel : ViewModel(){

private val job = SupervisorJob()

protected val uiScope = CoroutineScope(Dispathcers.Main + job)

override fun onCleared(){

super.onCleared()

uiScope.coroutineContext.cancelChildren()

}

}

//使用

class MyViewModel : ScopedViewModel(){

private fun loadData() = uiScope.launch {

val result = withContext(bgDispatcher) {

// your blocking call

}

}

}

四、如何處理協程中的異常

4.1 try-catch

我們可以使用try-catcher捕獲并處理異常

private fun loadData() = GlobalScope.launch(uiDispatcher) {

view.showLoading()

try {

val result = withContext(bgDispatcher) { dataProvider.loadData() }

view.showData(result)

} catch(e: Exception){

e.printStackTrace()

}

}

為了避免在Presenter中使用try-catch,最好在dataProvider.loadData()函數中處理異常并使其傳回通用Result類。

data class Result(val success: T? = null,

val error: Throwable? = null)

private fun loadData() = launch(uiContext){

view.showLoading()

val task = async(bgContext) { dataProvider.loadData("Task") }

val result: Result = task.await()

if(result.success != null){

view.showData(result.success)

} else if(result.error != null){

result.error.printStackTrace()

}

}

4.2 async parent

使用async啟動父協程來忽視異常。

private fun loadData() = GlobalScope.async(uiDispatcher) {

view.showLoading()

val result = withContext(bgDispatcher) { dataProvider.loadData() }

view.showData(result)

}

使用這種方法, 異常會被儲存在job對象中。我們可以使用invokeOnCompletion()方法來取回它。

var job: Job? = null

fun startPresenting() {

job = loadData()

job?.invokeOnCompletion { it: Throwable? ->

it?.printStackTrace()

job?.getCompletionException()?.printStackTrace()

}

4.3 launch + coroutine exception handler

你可以将CoroutineExceptionHandler添加到父協同程式上下文以捕獲異常并處理它們。

val exceptionHandler: CoroutineContext = CoroutineExceptionHandler {

-, throwable->

view.showData(throwable.message)

job = Job()

}

private fun loadData() = GlobalScope.async(uiDispatcher + exceptionHandler){

view.showLoading()

val result = withContext(bgDispatcher) { dataProvider.loadData() }

view.showData(result) //如果發生異常就不會被調用

}

五、如何測試協程

啟動一個協程需要你指定一個CoroutineDispatcher。

class MainPresenter(val view: MainView,

val dataProvider: DataProviderAPI) {

private fun loadData() = GlobalScope.launch(Dispacthers.Main){

view.showLoading()

val result = withContetx(Dispatchers.IO) { dataProvider.loadData() }

view.showData(result)

}

}

如果你想為上面的MainPresenter編寫一個單元測試,你可能需要指定一個協程context用于ui和background執行。

可能最簡單的方法是向MainPresenter構造函數添加兩個參數:uiDispatcher,預設值為Main,ioContext,預設值為IO。

class MainPresnter(val view: MainView,

val dataProvider: DataProviderAPI,

val uiDispatcher: CoroutineDispatcher = UI,

val ioDispatcher: CoroutineDispatcher = IO){

private fun loadData() = GlobalScope.launch(uiDispatcher) {

view.showLoading()

val result = withContext(ioDispatcher) { dataProvider.loadData() }

view.showData(result)

}

}

現在,您可以通過提供Unconfined來輕松測試您的MainPresenter類,它隻會在目前線程上執行代碼。

@Test

fun startPresenting(){

//given

val view = mock(MainView::class.java)

val dataProvider = mock(DataProviderAPI::class.java)

val presenter = MainPresenter(view,

dataProvider,

Dispatcher.Unconfined,

Dispacther.Unconfined)

//when

presenter.startPresenting()

//then

}

六、如何實作協程線程日志

要了解哪個協同程式執行目前工作,可以通過System.setProperty打開調試工具并通過Thread.currentThread().name來記錄線程名稱。

//調式模式

System.setProperty("kotlinx.coroutines.debug", if(BuildConfig.DEBUG) "on" else "off")

launch(UI) {

log("Data loading started")

val task1 = async { log("Hello") }

val task2 = async { log("World") }

val result = task1.await() + task2.await()

log("Data loading completed: $result")

}

fun log(msg: String){

Log.d(TAG, "[${Thread.currentThread().name}] $msg")

}