Kotlin Jetpack 实战 | 08. 协程“不为人知”的调试技巧

9,231 阅读5分钟

往期文章

《Kotlin Jetpack 实战:开篇》

《00. 写给 Java 开发者的 Kotlin 入坑指南》

《03. Kotlin 编程的三重境界》

《04. Kotlin 高阶函数》

《05. Kotlin 泛型》

《06. Kotlin 扩展》

《07. Kotlin 委托》

前言

协程(Coroutines),是个让人又爱又恨的东西。代码写起来是真的,调试起来是真的

本文将介绍 Kotlin 协程的调试技巧,不会涉及太多协程实际内容。所以,不管你有没有协程的基础,都可以看下去,如果有遇到不懂的概念直接忽略即可,后面我会系统讲解。

这篇文章是为我们协程系列打基础的,后面进入我们的协程部分《图解协程》,具体的写作计划大家可以到这里看看:《Kotlin Jetpack 实战:目录》,欢迎提建议。

2. 前期准备

  • 将 Android Studio 版本升级到最新
  • 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开: github.com/chaxiu/Kotl…
  • 切换到分支:chapter_08_coroutine_debug
  • 强烈建议各位小伙伴小伙伴跟着本文一起实战,实战才是本文的精髓

3. 协程 JVM 参数

协程,可以理解为轻量级的线程。协程跟线程的关系,有点像“线程与进程的关系”。

在我们写 Java 并发代码的时候,我们经常会用 Thread.currentThread().name 带打印出当前线程的名字,然后通过日志来查看运行效果。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    final int index = i;
    fixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + "index = " + index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

/*
pool-1-thread-1index = 0
pool-1-thread-3index = 2
pool-1-thread-2index = 1
pool-1-thread-2index = 3
pool-1-thread-3index = 4
*/

对于 Kotlin 协程,我们也可以做类似的事情。具体做法是,添加 JVM 参数:-Dkotlinx.coroutines.debug。具体做法如下:

  • 1.点击 IDE 的 Edit Configurations

  • 2.将-Dkotlinx.coroutines.debug填入 VM options 中:

在加上这个参数以后, 我们通过 Thread.currentThread().name,就会自动为我们输出当前协程的名字。

// 这段代码看不懂没关系,忽略即可
// 请直接看后面的输出信息和注释
fun main() {
    runBlocking<Unit> {
        fun log(msg: Any) {
            println("${Thread.currentThread().name} msg=$msg")
        }

        log(1)

        launch {
            val a = 4
            delay(300)
            log(a)
        }
        launch {
            val b = 3
            delay(200)
            log(b)
        }
        launch {
            val c = 2
            delay(100)
            log(c)
        }
    }
}

/*
// 输出:

1个线程   4个协程
 ↓         ↓
main @coroutine#1 msg=1
main @coroutine#4 msg=2
main @coroutine#3 msg=3
main @coroutine#2 msg=4
*/

小结

  • 一个线程可以对应多个协程
  • 协程将线程划分成更小的单元
  • 协程跟线程的关系,有点像“线程与进程的关系”。

看到这里,也许会有小伙伴说:“我还是搞不懂协程到底是什么,输出一个日志,能说明什么问题呢?”

确实,我在刚学习协程的时候,也有这种苦恼,协程太抽象了,比线程还抽象。线程它起码有一个 Thread.java 的源码给你看,协程呢,没有,Kotlin 编译器将它底层细节都屏蔽了。

那么,有没有更直观的方式来调试协程呢?有的,只是目前国内知道的人应该不多。

前几天我在 Kotlin 官方的博客 中了解到:Kotlin 官方在 1.4 版本中为协程调试增加了许多支持。虽然 Kotlin 1.4 现在还没正式发布,但是我们可以抢先体验它的 RC 版本(Release Candidate Version)。

4. Kotlin 1.4 协程调试

Kotlin 1.3 最让我惊喜的是 Flow,而 Kotlin 1.4 最让我惊喜的则是“协程调试支持”

在 Kotlin 1.4 之前,我一直都是通过 JVM 参数来研究协程的,这种方式并不友好,有的时候为了理解协程的代码,我需要加很多无关的 log。

一起看看 Kotlin 1.4 的变化:

Kotlin 1.4 之前Kotlin 1.4
断点断点经常不生效稳定
单步调试单步调试经常不生效稳定
单独的协程调试窗口不支持支持
查看协程创建栈不支持支持
查看协程调用栈不支持支持
协程挂起状态不支持支持
协程内存信息 dump不支持支持

虽然 Kotlin 1.4 尚未发布,但这并不影响我们提前探索使用 Kotlin 1.4,毕竟早用早享受嘛。

4-1 升级 Kotlin 版本

首先,将 Kotlin 相关的库升级到最新的:1.4-RC

    const val kotlinVersion = "1.4.0-rc"
    const val coroutines = "1.3.8-1.4.0-rc"

4-2 Kotlin EAP 渠道

接下来,我们需要升级 IDE 自带的 Kotlin 插件版本:

  • 1.首先进入:Configure Kotlin Plugin Updates

  • 2.然后,在接下来的页面中,在 Updates channel 下拉框中选择:1.4.x,点击 check(需要梯子),接着 IDE 会提示你有新的 Kotlin 版本,你点击:install 安装,然后重启即可。(如果你在看这篇文章的时候,1.4 正式版已经发布了的话,这一步就可以省略了。你只需要确保 IDE 是最新的即可。)

4-3 协程的断点设置

  • 3.接下来在 IDE 中找到我们的示例代码,打一个断点,然后右键点击断点,在弹出框中按照如下设置:

4-4 协程调试窗口

  • 4.然后就可以开始调试了,等程序停留在断点的位置后,会是这样。到目前为止,都还没有看见我们想要的协程调试窗,这时候需要我们手动点击红色箭头的位置:

  • 5.在弹出的窗口中,我们需要勾选 coroutines:

4-5 协程的调试栈

  • 6.勾选以后,我们就立马能看到一个专属于协程的调试框:

在上面的调试框中,我们能看到我们的代码当前有三个协程,其中1的位置,代表:corouine2, coroutine3 的状态是 Suspend2的位置,代表corouine4 的状态是 Running3的位置,是该协程的创建栈。

4-6 协程 Dump

  • 7.正如我前面所说的,协程跟线程很像,所以它们两者的调试框也是类似的。比如,你还可以 Dump 当前协程的内存状态,方便分析。

由于我们使用的并非正式的版本,Dump 出来的信息还比较少,相信之后官方对协程调试的支持会越来越好。

5. 总结

  • 工欲善其事,必先利其器。在正式学习协程之前,我们先学会协程调试技巧,这将对我们后面学习协程有极大的帮助。
  • 协程,并不是什么洪水猛兽,你完全可以将它看作一种更轻量级的线程。这句话我以后会反复讲,有些东西看起来复杂,本质却极其简单,协程就是这样的。
  • 本文使用的是 1.4-rc 版本,写博客做研究是没问题的,但在生产环境是不推荐使用的。

都看到这了,给点个赞呗!

下一章节,我们将开启我们的《图解协程》之旅。

回目录-->【Kotlin Jetpack 实战】