从Kotlin协程的实战 看 kotlin与java 在异常处理上的不同(Checked Exception)

2,018 阅读6分钟

kotlin协程是什么?

kotlin-jvm 这套东西上的协程 其实就是个线程框架。 与go 语言那种高性能的协程是有本质不同的,千万不要被迷惑了。除了阿里巴巴自己魔改的jvm以外,目前没有哪家jvm可以实现类似于go语言的那种协程能力。

kotlin中的suspend 关键字是什么作用?

他最大的作用就是 如果你在某个方法前面加了suspend 那么这个方法的执行就会要求一个线程的执行上下文环境,否则会编译不通过。

看上图中的例子,被suspend 标识的io1 方法, 如果没有指定线程的执行环境 那么ide报红直接 编译不过, 但是如果你指定了他的线程上下文环境 则是可以编译通过的。

这里要注意的是 如果你用suspend方法标记的函数 函数体内部 并没有使用协程关键字用来切线程的话, 那在编译的时候 suspend其实就没作用了,这里看图也可以知道 suspend在ide中显示成了灰色。

此外,所谓的kotlin协程的挂起 就是指切了个线程去干活,仅此而已。

kotlin-jvm 平台的协程到底有啥用?

其实就是方便给我们切线程使用的,假设我们有一个需求是简单的从本地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 协程的最大作用了。

其余的什么协程速度快啊之类的话 都是吹牛逼的。至少在kotlin-jvm上是不对的

使用协程会内存泄露吗?

当然会,因为协程 前面我们说过了,本质上就是个线程。 所以线程会导致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函数 比thread的sleep函数更加高效?

纯扯淡的说法,什么delay 不阻塞线程, sleep阻塞 线程 都是纯扯淡的说法,不要信。

本质上就是kotlin的delay函数 就是个协程,delay的本质就是切了个线程 然后在那个线程里面 sleep 了一段时间, 结束以后再切回来,仅此而已。

如果你自己读懂了我上面的文章 那么看到这个delay函数前面的suspend 其实你就知道是啥意思了。

retrofit支持kotlin了以后 写起来很简单?

确实很简单。可以看一下之前的写法:

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

kotlin 与 java 在ce上的不同?

对于java 来说 ,如果你在方法内部 throw了 一个Exception 那你必须在方法签名的地方 主动用throws标记这个异常 并且在调用的时候 主动捕获,否则编译就会不通过。

看到没 这里是编译不过的。

改成这样就可以了。

调用的地方也是:

必须try catch 否则也是编译不过的。

但是要注意噢,如果是 RuntimeException 则不会出现上面的情况,如果你在方法体内部 throw 一个

RuntimeException ,那你方法签名的地方 不写throws 也是可以编译通过的。 调用的地方啥都不干 不写try catch 也没问题。 这里一定要注意。

最后就是坑爹的kotlin语言,不管你在方法里面 throw的 是 RuntimeException 还是Exception 他都让你编译通过。。。

这就会导致 如果你调用一个函数,如果里面throw了一个异常,然后函数的注释或者文档又没告诉你这里可能会抛异常的话,那这个地方 可能就会发生可怕的事情了,对于客户端来说就是crush。 目前我不知道kotlin为啥要这样设计,但是大家在调用kotlin的函数的时候最好还是多点进去看看,看看到底会不会抛异常,免的搞出线上事故。