Android协程——入门

作者: Jotyy | 来源:发表于2019-07-11 10:42 被阅读156次

    [TOC]

    一、如何使用协程

    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")
    }
    

    相关文章

      网友评论

        本文标题:Android协程——入门

        本文链接:https://www.haomeiwen.com/subject/golrkctx.html