kotlin-jvm 这套东西上的协程 其实就是个线程框架。 与go 语言那种高性能的协程是有本质不同的,千万不要被迷惑了。除了阿里巴巴自己魔改的jvm以外,目前没有哪家jvm可以实现类似于go语言的那种协程能力。
他最大的作用就是 如果你在某个方法前面加了suspend 那么这个方法的执行就会要求一个线程的执行上下文环境,否则会编译不通过。
看上图中的例子,被suspend 标识的io1 方法, 如果没有指定线程的执行环境 那么ide报红直接 编译不过, 但是如果你指定了他的线程上下文环境 则是可以编译通过的。
这里要注意的是 如果你用suspend方法标记的函数 函数体内部 并没有使用协程关键字用来切线程的话, 那在编译的时候 suspend其实就没作用了,这里看图也可以知道 suspend在ide中显示成了灰色。
其实就是方便给我们切线程使用的,假设我们有一个需求是简单的从本地sd卡上请求一个数据 然后显示在ui上
纯java版本的
new Thread() { @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { } runOnUiThread(new Runnable() { @Override public void run() { mGoodsTitle.setText("adasdasda"); } }); } }.start(); 复制代码
如果这个需求再复杂一点 ,比如需要你显示在ui上以后 再去网络请求个什么东西 然后接口回来再刷新ui。 这一套如果用原生的java来写,可想而知会很麻烦,而且代码会有很多回调。可读性也不好。
但是如果用Rxjava来写,则会简单很多
subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) 复制代码
我们可以用 rxjava提供的这些操作符 来方便的切换我们的线程,代码可读性会变的非常好。
有人要问了,那还需要kotlin的协程干啥?
kotlin的协程在处理类似代码的时候 会更加智能, 他的线程切走了以后 是可以自动切回来的 不需要你再手动的指定你的代码执行线程了。
举个例子:
GlobalScope.launch(Dispatchers.Main) { withContext(Dispatchers.IO) { //do io sth } textview.text = " do ui sth" withContext(Dispatchers.IO) { //do io sth2 } textview.text = " do ui sth2" } 复制代码
这里就是典型的kotlin的 协程应用场景。 我们先指定了一个线程执行上下文的环境 是main 也就是主线程。
然后我们用协程的关键字 withContext 来切到io线程执行我们的工作, 这个时候 后面的代码是不会走的。
一定会等到withContext里面的代码执行完毕以后, 才会走到 textview.text = " do ui sth" 这里。
然后后面我们又执行了 withContext(Dispatchers.IO) 一次 ,这时 最后一行的textview.text 也是不会走的。
他一定会等到 上面的协程执行完毕以后 才会走。
所以这里你好好体会下 是不是会觉得这种写法 比rxjava还要方便? 毕竟线程切走以后 再切回来这个操作 kotlin协程帮我们做好了,再也不需要手动执行线程了 这个就是kotlin 协程的最大作用了。
当然会,因为协程 前面我们说过了,本质上就是个线程。 所以线程会导致activity内存泄露的场景 在协程中一样存在。 其实这里如果有rxjava 编程经验的同学 应该大概知道怎么做了。这里rxjava的写法我就不多写了,写一下协程的吧。
首先定义一个scope
var scope= MainScope(); 复制代码
然后稍微改变一下我们的写法 注意这次不是global scope了 是我们刚才定义的scope了
scope.launch(Dispatchers.Main) { withContext(Dispatchers.IO) { //do io sth } textview.text = " do ui sth" withContext(Dispatchers.IO) { //do io sth2 } textview.text = " do ui sth2" } 复制代码
然后在这里 释放即可。
override fun onDestroy() { scope.cancel() super.onDestroy() } 复制代码
跟Rxjava十分的像。就是换了个写法而已。 当然你如果还使用了lifecycle
那就更加简单了
lifecycleScope.launch { } 复制代码
这样释放的时候 我们连cancel 都可以省略了, 全部交给谷歌提供的lifecycleScope就可以了。
最后提一下 coroutineScope 这个函数 也相当有用,可以限制我们的协程执行环境 ,有兴趣的同学可以自行搜索一下,这里不再展开。
纯扯淡的说法,什么delay 不阻塞线程, sleep阻塞 线程 都是纯扯淡的说法,不要信。
本质上就是kotlin的delay函数 就是个协程,delay的本质就是切了个线程 然后在那个线程里面 sleep 了一段时间, 结束以后再切回来,仅此而已。
如果你自己读懂了我上面的文章 那么看到这个delay函数前面的suspend 其实你就知道是啥意思了。
确实很简单。可以看一下之前的写法:
这是java版本的:
我们看下kotlin协程版本的:
但是这里要注意了,这里有个大坑,kotlin是一个 不强制异常处理检查的语言,所以这里的异常 是会抛出来的! 一定要注意,一旦网络请求有了诸如404 或者502的异常 你上面的代码就直接crush了。
其实想想也能想明白 你看我这个简写的写法 甚至都没有地方可以处理onFailure 里面的流程,那万一发生了 onFailure里面的情况怎么办? 怎么把这些错误信息抛给你? 也只有异常了。
看下retrofit的 源码
一目了然。
所以使用retrofit+kotlin的时候 一定要谨记 主动捕获异常,一方面是为了程序不要crush 另外一方面也是为了 处理程序中的异常情况
GlobalScope.launch(Dispatchers.Main) { val reposItem = try { retrofit.create(GitHubService::class.java) .allRepositories2(page, "pushed:<" + Date().format("yyyy-MM-dd"), 10) } catch (e: Exception) { Log.v( "wuyue", "e:" + (e is HttpException) + " e:" + e.message + " e:" + (e is Throwable) ); } Log.v("wuyue", "reposItem2222222==$reposItem") } 复制代码
对于java 来说 ,如果你在方法内部 throw了 一个Exception 那你必须在方法签名的地方 主动用throws标记这个异常 并且在调用的时候 主动捕获,否则编译就会不通过。
看到没 这里是编译不过的。
改成这样就可以了。
调用的地方也是:
必须try catch 否则也是编译不过的。
但是要注意噢,如果是 RuntimeException 则不会出现上面的情况,如果你在方法体内部 throw 一个
RuntimeException ,那你方法签名的地方 不写throws 也是可以编译通过的。 调用的地方啥都不干 不写try catch 也没问题。 这里一定要注意。
最后就是坑爹的kotlin语言,不管你在方法里面 throw的 是 RuntimeException 还是Exception 他都让你编译通过。。。
这就会导致 如果你调用一个函数,如果里面throw了一个异常,然后函数的注释或者文档又没告诉你这里可能会抛异常的话,那这个地方 可能就会发生可怕的事情了,对于客户端来说就是crush。 目前我不知道kotlin为啥要这样设计, 但是大家在调用kotlin的函数的时候最好还是多点进去看看,看看到底会不会抛异常,免的搞出线上事故。