Koltin系列 - 协程从认识到安卓中的使用(五)

2020-03-23  本文已影响0人  未扬帆的小船

前言

学习了Kotlin一整个系列了,但是协程这块迟迟没有整理成一篇博文。诶,最近状态有点不对 >_< || 。
但是无论如何,一定要加油!!最后一篇要划上个完美点的句号,撒个漂亮点的花。

关于协程的一个点在这里跟大家先说一下,协程并非什么很深奥的东西,说白了也是在线程上面的产物,并非凭空产生的一个新的概念。官网讲得可能有点高大上了,不过实际上你就当是它帮我们使用了线程池Handler进行一些自动切换线程的逻辑封装进而形成了这样子的一种API吧~~

协程的一些基础使用

添加基本的依赖

implementation  'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'

GlobalScope

官网定义:Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely. Another use of the global scope is operators running in Dispatchers.Unconfined, which don’t have any job associated with them.
Application code usually should use an application-defined CoroutineScope. Using async or launch on the instance of GlobalScope is highly discouraged.
大致意思就是:这个一般被用于顶级的协程,application生命周期级别的,不会过早的被取消。应用程序通常应该使用一个应用程序定义的CoroutineScope。使用异步或启动的实例GlobalScope非常气馁(不建议的)。

先来模拟一个场景,在一个ActivityA调用globalScopeLaunch,或者globalScopeLaunch进行耗时操作,类似IO操作或者网络请求等。然后在它还没有返回的时候销毁ActivityA再跳转到ActivityB

fun globalScopeLaunch(){
        GlobalScope.launch(Dispatchers.Main) {
            delay(5000)
            Toast.makeText(this@MainActivity,"等待五秒弹出~~~",Toast.LENGTH_LONG).show()
        }
    }

你会发现,它照样会弹出这个Toast,但是这样子其实并非我们想要的结果。有些事务我们应该随着组件的生命周期结束而结束。否则一会造成资源的浪费或者内存泄露。(这里有个问题,如果你在生命周期结束的时候手动关闭的话,那就可以避免这种情况。但是这里就涉及到要你自己手动来控制了)

 private fun globalScopeLaunch1(){
        GlobalScope.launch(Dispatchers.Main) {
            launch (Dispatchers.Main){
                delay(1000)
                Toast.makeText(this@MainActivity,"等待一秒弹出",Toast.LENGTH_LONG).show()
            }
            Toast.makeText(this@MainActivity,"立马弹出~~~",Toast.LENGTH_LONG).show()
            delay(5000)
            Toast.makeText(this@MainActivity,"等待五秒弹出~~~",Toast.LENGTH_LONG).show()
       }
    }

globalScopeLaunch1中,立马弹出~~~ ->等待一秒弹出 ->"等待五秒弹出~~~。这里之所以会先弹出来立马弹出~~~这个信息。因为协程中,又开了一个新的协程,新的协程阻塞一秒不关外边协程的事情,外边协程继续执行。

private fun globalScopeLaunch(){
        job =  GlobalScope.launch(Dispatchers.Main) {
            launch (Dispatchers.Main){
                runBlocking {//加了runBlocking这个协程作用域
                delay(1000)
                Toast.makeText(this@MainActivity,"等待一秒弹出",Toast.LENGTH_LONG).show()
                }
            }
            Toast.makeText(this@MainActivity,"立马弹出~~~",Toast.LENGTH_LONG).show()
            delay(5000)
            Toast.makeText(this@MainActivity,"等待五秒弹出~~~",Toast.LENGTH_LONG).show()
       }
    }

runBlocking会阻塞导致立马弹出~~~这个Toast不会立刻显示出来,而是等了1秒后,再弹出来。

上面的代码可以简化一下
在协程作用域中,可以使用withContext(Dispatchers.Main)替换launch (Dispatchers.Main)

job =  GlobalScope.launch(Dispatchers.Main) {
            withContext(Dispatchers.Main){} 
}

协程作用域

GlobalScope.launch(Dispatchers.Main)这里我是分发到主线程Main上面进行delay但是并不会造成ANR,可以简单看一下launch怎么调用的

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

第三个参数 : block: suspend CoroutineScope.() 表示使用的协程作用域是CoroutineScope并不会造成阻塞。这里的阻塞是指协程作用域外的代码阻塞,协程作用域内部还会被阻塞的。
CoroutineScopeGlobalScope的父类~

两种协程作用域,以及结构化并发.png

协程的启动方式launch与Async

private fun globalScopeAsync(){
  GlobalScope.launch(Dispatchers.Main){
  val deferred = async(IO) {
                Thread.sleep(5000)
                "等待五秒弹出~~~"
            }
    Toast.makeText(this@MainActivity,"立马先弹出来~~",Toast.LENGTH_LONG).show()//这句是来验证sync是不会阻塞改async协程外的代码的
    val message =  deferred.await()
    Toast.makeText(this@MainActivity,message,Toast.LENGTH_LONG).show()
  }
}

async会异步跑该作用域外层的协程的逻辑,我们可以看到"立马先弹出来~~"弹出框会先弹出来,再等过五秒在弹出 "等待五秒弹出~~~"再弹出来。在await这里会阻塞等待deferred返回回来再继续接下来的操作。

协程的启动方式.png

协程分发

image.png

协程的取消

获取到对应协程的Job对象,调用cancel()

var job = GlobalScope.launch(Dispatchers.Main) { }
job.cancel()
//Deferred的对象父类是Job
var deferred =  GlobalScope.async {  }
deferred.cancel()

Android上使用协程的正确姿势

MainScope

上面Global的官方定义中已经提示我们使用自定义的协程。
MainScopekotlin为我们自定义好的一个协程作用域。
代码定义:

@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

基本使用:

class MyAndroidActivity {
  private val scope = MainScope()
 //使用MainScope并赋予它名字
// val mainScope = MainScope() + CoroutineName(this.javaClass.name)
  private fun mainScopeLaunch(){
        scope.launch {}
  }
  override fun onDestroy() {
    super.onDestroy()
    scope.cancel()
  }
}

可以将这逻辑放到base类中

//无需定义协程名字的时候
open class BaseCoroutineScopeActivity : AppCompatActivity() , CoroutineScope by MainScope()

class MainActivity : BaseCoroutineScopeActivity(){

 private fun mainScopeLaunch(){
        launch {  }
    }
override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}
-----------------------------------------------------------------
//定义协程名字的时候
open class BaseCoroutineScopeActivity : AppCompatActivity() {
    val mainLaunch =  MainScope()+ CoroutineName(this.javaClass.simpleName)
}

class MainActivity : BaseCoroutineScopeActivity(){
 private fun mainScopeLaunch(){
        mainLaunch.launch {  }
    }
override fun onDestroy() {
        super.onDestroy()
        mainLaunch.cancel()
    }
}

ViewModelScope

使用该协程首先要导入包

api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc02'

代码定义:

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

这段代码跟MainScope一样,只是外面多了一层CloseableCoroutineScope的封装,这个是为什么呢??
我们进去setTagIfAbsent看一下

   <T> T setTagIfAbsent(String key, T newValue) {
        T previous;
        synchronized (mBagOfTags) {
            previous = (T) mBagOfTags.get(key);
            if (previous == null) {
                mBagOfTags.put(key, newValue);
            }
        }
        T result = previous == null ? newValue : previous;
        if (mCleared) {
            closeWithRuntimeException(result);
        }
        return result;
    }

这里可以看出,当mCleared = true的时候它会自动帮我们关闭掉viewModelScope,也就是它帮我们处理生命周期的问题了 我们只管使用就可以。

使用:

fun requestAhuInfo() {
         viewModelScope.launch {         }
    }

LiveData && LifecycleScope 这两个我自己并没有使用。

推荐一下

秉心说TM的 - 如何正确的在 Android 上使用协程 ?
里面有说了这几种kotlin为我们提供的协程

协程中的多种任务情况

        viewModelScope.launch {
            var result1 = withContext(Dispatchers.IO) {
                Log.i(TAG, "result1-1")
                Log.i(TAG, "result1-2")
                Thread.sleep(4000)
                Log.i(TAG, "result1-3")
                "Hello"
            }
            var result2 = withContext(Dispatchers.IO) {
                Log.i(TAG, "result2-1")
                Log.i(TAG, "result2-2")
                Log.i(TAG, "result2-3")
                "world"
            }
            val result = result1 + result2
            Log.i(TAG, result)
        }
------------------------------------------------------------------------------------
2020-03-23 19:19:40.587 11021-11096/com.ldr.testcoroutines I/MainViewModel: result1-1
2020-03-23 19:19:40.587 11021-11096/com.ldr.testcoroutines I/MainViewModel: result1-2
2020-03-23 19:19:44.592 11021-11096/com.ldr.testcoroutines I/MainViewModel: result1-3
2020-03-23 19:19:44.602 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-1
2020-03-23 19:19:44.603 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-2
2020-03-23 19:19:44.603 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-3
2020-03-23 19:19:44.603 11021-11021/com.ldr.testcoroutines I/MainViewModel: Helloworld
        viewModelScope.launch {
          val deferred =  async {
                Thread.sleep(4000)
                Log.i(TAG, "result1-1")
                Log.i(TAG, "result1-2")
                Log.i(TAG, "result1-3")
                "Hello"
            }
            val deferred1 = async {
                Log.i(TAG, "result2-1")
                Log.i(TAG, "result2-2")
                Log.i(TAG, "result2-3")
                "world"
            }
            var str = deferred.await() + deferred1.await()
            Log.i(TAG, str)
        }
------------------------------------------------------------------------------------
2020-03-23 18:52:28.016 9155-9155/com.ldr.testcoroutines I/MainViewModel: result1-1
2020-03-23 18:52:28.016 9155-9155/com.ldr.testcoroutines I/MainViewModel: result1-2
2020-03-23 18:52:28.016 9155-9155/com.ldr.testcoroutines I/MainViewModel: result2-3
2020-03-23 18:52:28.018 9155-9155/com.ldr.testcoroutines I/MainViewModel: result2-1
2020-03-23 18:52:28.018 9155-9155/com.ldr.testcoroutines I/MainViewModel: result2-2
2020-03-23 18:52:28.018 9155-9155/com.ldr.testcoroutines I/MainViewModel: result2-3
2020-03-23 18:52:28.018 9155-9155/com.ldr.testcoroutines I/MainViewModel: Helloworld
  viewModelScope.launch {
            launch(Dispatchers.IO) {
                Log.i(TAG, "result1-1")
                Log.i(TAG, "result1-2")
                Thread.sleep(4000)
                Log.i(TAG, "result1-3")
            }

            launch(Dispatchers.IO) {
                Log.i(TAG, "result2-1")
                Log.i(TAG, "result2-2")
                Log.i(TAG, "result2-3")
            }
        }
---------------------------------------------------------------------------------
2020-03-23 19:21:29.781 11021-11128/com.ldr.testcoroutines I/MainViewModel: result1-1
2020-03-23 19:21:29.781 11021-11128/com.ldr.testcoroutines I/MainViewModel: result1-2
2020-03-23 19:21:29.782 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-1
2020-03-23 19:21:29.782 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-2
2020-03-23 19:21:29.782 11021-11096/com.ldr.testcoroutines I/MainViewModel: result2-3
2020-03-23 19:21:33.782 11021-11128/com.ldr.testcoroutines I/MainViewModel: result1-3

协程的异常处理

  val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }

    fun catchFun(): Unit {
        viewModelScope.launch(handler) {
            throw IOException()
        }
    }

 fun catch2Fun(): Unit {
        viewModelScope.launch(handler) {
            launch(Dispatchers.IO) {
                withContext(Dispatchers.Main){
                    throw IOException()
                }
            }
        }
    }
----------------------------------------------------------------------------------
2020-03-23 19:57:21.205 12038-12096/com.ldr.testcoroutines I/System.out: Caught java.io.IOException
2020-03-23 19:59:23.221 12038-12096/com.ldr.testcoroutines I/System.out: Caught java.io.IOException

经过上面的测试,可以知道CoroutineExceptionHandler这种方法可以将多层嵌套下的异常也捕获到。

//错误的写法 协程外部是捕获不到异常的
    fun catch1Fun(): Unit {
        try {
            viewModelScope.launch(Dispatchers.Main) {
                throw IOException()
            }
        }catch (e:Exception){
            Log.i(this.javaClass.name,e.cause?.message?:"抛出了异常")
        }
    }
//正确的写法 好吧,,,,我觉得我在说废话。。。
 fun catch1Fun(): Unit {
  viewModelScope.launch(Dispatchers.Main) {
     try {
           throw IOException()
        }catch (e:Exception){
            Log.i(this.javaClass.name,e.cause?.message?:"抛出了异常")
        }
    }
 }

附带源码地址: https://github.com/lovebluedan/TestCoroutines

总结

以上就是简单的介绍了一下,协程的一些基本用法,关于里面很多原理性的东西,以后有机会再写吧~~ 说实话,我并没有用很深入,所以很多细节的东西还没理解好。以往可以写的深奥点,少点废话少点代码,文章写得精炼点~~

上一篇下一篇

猜你喜欢

热点阅读