Android协程——入门

Demo地址:https://github.com/jotyy/coroutines-retrofit-example 欢迎交流和star,谢谢

一、如何使用协程

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<out T>(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<String> = 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")
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容