【AOSP】基于Pixel5编译刷写Android车机系统

【AOSP】基于Pixel5编译刷写Android车机系统

本文介绍了使用Pixel5手机作为车载Android开发设备,从源码拉取,到系统编译,刷写的全流程记录。并且当时处于WSL子系统环境,并非全功能Ubuntu系统。

Pixel5使用体验

去年2022年中的时候网购了一台Pixel5的库存机,现在闲置成备用机了。

之所以有这个在手机上跑车机的想法,是因为笔者是车机Android应用层开发,想着谷歌手机原生支持那么好,能不能整一个Google Automotive的车机系统上去跑跑,顺便还可以学习学习AOSP源码、系统编译、系统apk集成、权限管理、CarService服务等等。

一看官方网站居然还真有定制,而且目前恰好支持Pixel4a和Pixel5,另外还有Pixel6,但是是Experimental实验性的,拿6代设备的朋友整活有风险。

2024-12-31更新:目前已经支持到了Pixel8手机

废话不多说,开始正经的经验记录!

系统环境准备

首先最低硬盘控件需要准备300G,低于这个数就很危险了。

打开Windows功能

注意Google的AOSP开源项目,谷歌宣称其开发和调试均是在Ubuntu14上进行的。强烈建议开发者也需要使用Ubuntu系统进行AOSP源码的拉取和编译。

不想把自己电脑刷成Ubuntu系统的话,也可以使用windows上的wsl虚拟机,这个也是需要win10及以上可以使用,直接通过微软Microsoft应用商店搜索Ubuntu即可下载安装。注意安装之前要在控制面板的“程序和功能”里打开“windows子系统选项”,重启系统后生效。

WSL迁移其他盘与空间扩展

安装完成后,进行简单的username用户名和password设置就可以进入系统了,啊,还是熟悉的terminal指令。然后下一步我们需要将这个子系统的位置从C盘移出去。

因为安装位置默认在C盘,而一份源码下载和编译后至少需要300G的空间,所以为了windows系统的流畅运行,我们最好不要将其挤在C盘,使用工具将其迁移到其他盘下面。为了完成这个操作,我们需要下载一个第三方工具 LxRunOffline,这个是由国人开发的 WSL 工具,其可以弥补官方工具的不足,比如说他可以实现将任何发行版的 Linux 以 WSL 形式安装到 Windows 10 中,增强 WSL 发行版管理功能,甚至可以实现 WSL 系统备份和恢复,这样无论是学习 Linux 还是进行开发工作都要比以往操作更为方便。

# 以管理员权限打开PowerShell,首先关闭wsl虚拟机
wsl --shutdown

#切到LxRunOfflin目录下,查看系统里wsl有哪些
.\LxRunOffline.exe list


#迁移wsl,需要十几分钟,完成后会生成虚拟硬件磁盘ext4.vhdx文件
.\LxRunOffline.exe move -n Ubuntu-20.04 -d f:\wsl_ubuntu20

#迁移完成,查看迁移后路径
.\LxRunOffline.exe get-dir -n Ubuntu-20.04

完成后还有一个问题,WSL默认只支持最大256G的硬盘空间,我们下载源码编译后很有可能就会超过256G,那么WSL就会报错,编译等操作也会中断。想要将WSL的最大硬盘空间突破这个限制,需要通过扩展 VHD 大小来解决:

#关闭wsl
wsl --shutdown

#查看wsl版本
 wsl -l -v
  NAME            STATE           VERSION
* Ubuntu-20.04    Stopped         2

#进入disk命令行
diskpart

#选择虚拟磁盘
DISKPART> Select vdisk file=f:\wsl_ubuntu20\ext4.vhdx

#查看VHD的详细信息
DISKPART> detail vdisk

#扩展vdisk空间,xxx为空间大小,以MB为单位,默认为256000,我拓展到了1000000
DISKPART> expand vdisk maximum=xxx

#退出DISKPART,进入wsl
DISKPART> exit
$wsl

#查看分区
$df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb        251G  991M  238G   1% /
tools           200G   53G  148G  27% /init
none            4.9G     0  4.9G   0% /dev
tmpfs           4.9G     0  4.9G   0% /sys/fs/cgroup
none            4.9G  4.0K  4.9G   1% /run
...

#在wsl中操作,使wsl知道磁盘大小限制已经更改
$sudo mount -t devtmpfs none /dev
# 将none挂载到/dev目录下,若返回'mount: /dev: none already mounted on /dev.',可忽略
$mount | grep ext4
# 得到none挂载到/dev目录下的磁盘路径名
# 本句命令返还的信息 '/dev/sdX' 即为磁盘路径名,X可能是a,b,c等,xxx为前面分配的vhd大小,M为MB单位
$ sudo resize2fs /dev/sdb 1000000M
resize2fs 1.44.1 (24-Mar-2018)
Filesystem at /dev/sdb is mounted on /; on-line resizing required
old_desc_blocks = 32, new_desc_blocks = 123
The filesystem on /dev/sdb is now 256000000 (4k) blocks long.
# 重新查看分区配置
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb        961G 1000M  918G   1% /
tools           200G   53G  148G  27% /init
none            4.9G     0  4.9G   0% /dev

WSL拉取同步Android源码

上面WSL移出C盘和硬盘空间扩展完成之后,Ubuntu环境准备完成,即可开始Android源码的拉取了,注意拉代码前一定要提前下载这些辅助工具,以免正式开始后缺工具,手忙脚乱。

代码拉取前的程序安装

注意不要习惯性的将Ubuntu换源阿里或者中科大,我们直接使用WSL上自带的默认软件源,否则有些官方工具的安装会产生链式依赖问题,在Ubuntu18及以上终端输入:

sudo apt-get install git-core gnupg flex bison build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 libncurses5 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig

另外别忘了安装java,后面编译,还有开发系统应用生成Android系统platform签名,需要用到java的keytool工具。

sudo apt install oracle-java8-installer

WSL不可使用adb,刷机流程更改

使用wsl的话,我们虽然可以使用usbipd这个工具来配置,访问windows电脑连接的usb设备,但是不可以识别手机,也不可以在wsl上使用adb进行调试刷机。所以我最终采用的方案是Ubuntu编译,将编译产物同步到windows,再在windows上连接手机,最后进行设备刷写推送。

# 这个目录就是windows的文件夹在wsl的挂载同步,可以以此作为两个系统的文件同步区域
cd mnt/d/Pixel5

# 复制编译产物到Windows下的文件夹
cp -r /aaos/build/product/XXXX   /mnt/d/Pixel5

使用repo进行源码拉取同步

首先明确一点,Pixel 5手机其支持的车机版本只有一个,我们必须使用 Android 12,和build SP1A.210812.016.A1,对应AOSP分支为 android-12.0.0_r3

Android的AOSP源码使用repo来进行版本管理,repo是Google开发的用于管理Android版本库的一个工具,repo是使用Python对git进行了一定的封装,并不是用于取代git,它简化了对多个Git版本库的管理。用repo管理的版本库都需要使用git命令来进行操作。

下载repo工具:

mkdir ~/bin
PATH=~/bin:$PATH
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

建议在home的个人文件夹下,建立放置源码的工作目录,一开机ls就是它了:

# 新建文件夹
mkdir aaos_on_phone
# 切换工作目录
cd aaos_on_phone

为了下载速度能拉满,我没有使用谷歌的官方仓库来拉取同步代码,而是改为使用清华大学的镜像网站,内容是相同的:清华大学开源软件镜像站 | Tsinghua Open Source Mirror打开后,可以看到第一个就是AOSP项目。

tsinghua

在新建好的工作目录下,使用如下命令通过repo工具拉取AOSP源码,笔者没有WI-FI,直接使用手机流量来硬刚的,大概需要70个G左右,耗时2小时。

# 初始化repo仓库,拉取某一个特定的分支
repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-12.0.0_r3
# 开始同步拉取代码
repo sync

经过漫长的等待之后,打开工作目录,应该是下面的目录结构,因为我已经编译过,还加入了设备build文件,所以会多一点东西:

特定设备的二进制文件下载解包

源码拉取完成后,需下载特定设备的专有二进制文件和补丁程序,在如下网站找到对应设备与安卓版本的二进制包Nexus 和 Pixel 设备的驱动程序二进制文件  |  Google Play services  |  Google for Developers

对于Pixel 5,需要找到:适用于 Android 12.0.0 (SP1A.210812.016.A1) 的 Pixel 5 二进制文件。

下载完毕之后,将此文件copy到源码目录进行解压:

# 复制供应商映像和高通的驱动二进制文件到源码目录
cp mnt/d/Downloads/extract-google_devices-redfin.sh /home/stephen/aaos
cp mnt/d/Downloads/extract-qcom-redfin.sh /home/stephen/aaos

# 解压缩两个文件
curl --output - https://dl.google.com/dl/android/aosp/google_devices-redfin-sp1a.210812.016.a1-8813b219.tgz  | tar -xzvf -
tail -n +315 extract-google_devices-redfin.sh | tar -zxvf -

curl --output - https://dl.google.com/dl/android/aosp/qcom-redfin-sp1a.210812.016.a1-8d32b5b1.tgz | tar -xzvf -
tail -n +315 extract-qcom-redfin.sh | tar -xzvf -

开始编译源码

WSL运行内存分配

源码和设备二进制文件准备完成后,可能有的朋友就按耐不住要开始编译了,其实还有很重要的一个步骤。

源码的编译是非常非常耗性能的,特别是内存,默认分配的是物理机一半的运行内存,对于编译源码是不太够的,所以我们要对WSL子系统进行一些特殊的性能配置,在个人用户文件夹下,新建一个  .wslconfig  文件,里面配置的字段含义可以参考微软官方文档:WSL 中的高级设置配置 | Microsoft Learn

[wsl2]

# Limits VM memory to use no more than 24 GB, this can be set as whole numbers using GB or MB
memory=24GB
# Sets the VM to use 6 virtual processors
processors=6
# Sets amount of swap storage space to 8GB, default is 25% of available RAM
swap=0
# Turn on default connection to bind WSL 2 localhost to Windows localhost
localhostForwarding=true

# 一些实验性的配置
# Enable experimental features
[experimental]
autoMemoryReclaim=gradual  
networkingMode=mirrored
dnsTunneling=true
firewall=true
autoProxy=true

配置完成后,笔者电脑是32G内存,核显显存分出去1个G,虚拟机分配24G,合理设置保证性能同时不会使windows系统其他功能可用内存太局促。

切换到wsl的源码目录,准备开始编译。

launch起编系统

名词解释:

  • Makefile → Android平台编译系统,用Makefile写出来的一个独立项目,定义了编译规则,实现自动化编译,将分散在数百个Git库中的代码整合起来,统一编译,而且把产物分门别类地输出到一个目录,打包成手机ROM,还可以生成应用开发时使用的SDK、NDK等。
  • Android.mk → 定义一个模块的必要参数,使模块随着平台编译,简单点说就是告诉系统以什么规则编译源代码,并生成对应目标文件;
  • kati → Google专门为Android研发的小工具,基于Golang和C++,作用是:将Android中的Makefile转换为Ninja文件
  • Ninja → 致力于速度的小型编译系统,把Makefile看做高级语言,那它就是汇编,文件后缀为.ninja;
  • Android.bp → 替换Android.mk的配置文件;
  • Blueprint → 解析Android.bp文件翻译成Ninja语法文件;
  • Soong → Makefile编译系统的替代品,负责解析Android.bp文件,并将之转换为Ninja文件;

起编的命令不多,只有两三行:

# 预声明环境命令
. build/envsetup.sh
# 编译Pixel系统,target选择aosp_redfin_car
lunch <target>
# 开始make编译,新版上直接一个m即可
m
# 构建与汽车相关的软件包
m android.hardware.automotive.audiocontrol@1.0-service android.hardware.automotive.vehicle@2.0-service

每次开始编译开始的第一个命令便是. build/envsetup.sh。在文件envsetup.sh声明了当前会话终端可用的命令,这里需要注意的是当前会话终端,也就意味着每次新打开一个终端都必须再一次执行这些指令。build/envsetup.sh文件存在的意义就是,设置一些环境变量和shell函数为后续的编译工作做准备。

而后的lunch操作执行的其实就是build/envsetup.sh脚本中的lunch函数,选择一个版本进行编译,一般可选user,userdebug,eng三种版本,其上的权限是逐步升级的。如果launch后没有参数,那么会出现一列版本可供选择,选择对应版本前的数字即可。

最后m开始起编,过程很长,笔者第一次编译晚上11点开始,等了两小时才到40%,于是放下电脑睡觉去,早上醒来就编完了。在编译过程中,以前只在论坛文章里看到的那些类,现在全部在命令行里一个个闪现出来参与编译,站在上层应用开发者的角度来看,就很神奇。

源码单编某个模块

除了系统整体进行编译,我们也可以对单个应用模块进行编译,编完的apk可以push推送到系统对应文件夹下,完成单个模块的置换。

source build/envsetup.sh
lunch aosp_bonito-eng
#进入模块目录
cd package/apps/Setting

#编译单独模块的可选指令如下:
#mm → 编译当前目录下的模块,不编译依赖模块
#mmm → 编译指定目录下的模块,不编译依赖模块
#mma → 编译当前目录下的模块及其依赖项
#mmmma → 编译指定路径下所有模块,切包含依赖
mm

#编译成功会提示生成文件的存放路径,除了生成Setting.odex外,还会在
#priv-app/Settings目录下生成Settings.apk,可直接adb push或adb install
#安装APK验证效果,也可以使用make snod命令重新打包生成system.img,运行模拟器查看

开始刷机流程

AOSP编译产物

经过make编译后的产物,都位于源码的 /out目录 ,该目录下我们主要关注下面几个目录:

/out/host:Android开发工具的产物,包含SDK各种工具,比如adb,dex2oat,aapt等。 /out/target/common:通用的一些编译产物,包含Java应用代码和Java库; /out/target/product/[product_name]:针对特定设备的编译产物以及平台相关C/C++代码和二进制文件; 在/out/target/product/[product_name]目录下,有几个重量级的镜像文件:

  • system.img:挂载为根分区,主要包含Android OS的系统文件;
  • ramdisk.img:主要包含init.rc文件和配置文件等;
  • userdata.img:被挂载在/data,主要包含用户以及应用程序相关的数据; 当然还有boot.img,reocovery.img等镜像文件,这里就不介绍了。

查看/aaos/out/target/product/redfin文件夹下关于Pixel 5设备特定的文件:

stephen@CODE01:~/aaos/out/target/product/redfin$ ls
android-info.txt                           misc_info.txt
apex                                       module-info.json
appcompat                                  module-info.json.rsp
boot-debug.img                             obj
boot-test-harness.img                      obj_arm
boot.img                                   previous_build_config.mk
bootloader.img                             product
build_fingerprint.txt                      product.img
build_thumbprint.txt                       radio.img
clean_steps.mk                             ramdisk
data                                       ramdisk-debug.img
debug_ramdisk                              ramdisk-test-harness.img
dexpreopt_config                           ramdisk.img
dtb.img                                    recovery
dtbo.img                                   root
fake_packages                              super_empty.img
gen                                        symbols
installed-files-product.json               system
installed-files-product.txt                system.img
installed-files-ramdisk-debug.json         system_ext
installed-files-ramdisk-debug.txt          system_ext.img
installed-files-ramdisk.json               system_other
installed-files-ramdisk.txt                system_other.img
installed-files-recovery.json              test_harness_ramdisk
installed-files-recovery.txt               testcases
installed-files-root.json                  userdata.img
installed-files-root.txt                   vbmeta.img
installed-files-system-other.json          vbmeta_system.img
installed-files-system-other.txt           vendor
installed-files-system_ext.json            vendor.img
installed-files-system_ext.txt             vendor_boot-debug.img
installed-files-vendor-ramdisk-debug.json  vendor_boot-test-harness.img
installed-files-vendor-ramdisk-debug.txt   vendor_boot.img
installed-files-vendor-ramdisk.json        vendor_debug_ramdisk
installed-files-vendor-ramdisk.txt         vendor_ramdisk
installed-files.json                       vendor_ramdisk-debug.img
installed-files.txt                        vendor_ramdisk.img
kernel

确认无问题后,我把整个文件夹全部转到mnt挂载的windows目录下,准备好手机设备后即可刷写了。

cp -r ~/aaos/out/target/product/redfin /mnt/d/Pixel5

设置设备,刷写镜像文件 首先打开pixel 5的开发者选项里的USB调试模式,也需要打开OEM锁:

adb reboot bootloader
fastboot flashing unlock

在编译产物的文件夹,执行以下指令。开始清空设备数据,刷写车机系统,完成后推送汽车相关文件:

fastboot -w flashall
# 这些命令也可以制作成sh脚本,每次刷完机都执行一遍即可,免去手动输入
# 等刷写完毕并主屏幕显示后,再推送汽车专用文件
adb root
adb remount
adb reboot
# 每次刷写新系统都需要执行上面三步,使文件系统重新挂载生效
# 就可以使windows的shell获取root权限
adb root
adb remount
adb sync vendor
adb reboot

等手机再次reboot重启后就是下面的动画和launcher界面了: Google_mvi

后续

刷完了系统,不光是走完了一次体验,还需要找到可以学习的角度,深入改动系统代码,通过定制系统,达到需要的效果。

【Kotlin】Kotlin 2.2.0 文档阅读

【Kotlin】Kotlin 2.2.0 文档阅读

本文记录了在阅读Kotlin 2.2.0官方文档过程中的之前遗漏的内容,并且更加全面地了解Kotlin编译器的跨平台相关内容

原文链接:

Kotlin Language Documentation 2.2.0

查漏补缺

lambda作为函数参数类型

这个操作平时使用较少,将lambda作为返回参数类型的场景。例如:

val upperCaseString: (String) -> String = { text -> text.uppercase() }

fun main() {
println(upperCaseString("hello"))
// HELLO
}

将一个字符串对象全部转换为大写,将 upperCaseString 声明为lambda类型,就可以作为参数传递。

另外一种用法是作为返回的参数类型。

fun toSeconds(time: String): (Int) -> Int = when (time) {
"hour" -> { value -> value * 60 * 60 }
"minute" -> { value -> value * 60 }
"second" -> { value -> value }
else -> { value -> value }
}

fun main() {
val timesInMinutes = listOf(2, 10, 15, 1)
val min2sec = toSeconds("minute")
val totalTimeInSeconds = timesInMinutes.map(min2sec).sum()
println("Total time is $totalTimeInSeconds secs")
// Total time is 1680 secs
}

toSeconds 函数会根据传入的用法名称,返回一个lambda类型的函数,该函数将一个Int类型的参数转换为另一个Int类型的参数。

页码92

Multiplatform

【Kotlin】协程技术浅谈

【Kotlin】协程技术浅谈

本文从更全面的角度介绍Kotlin协程的设计思想,其相对线程有哪些更友好的优化

之前写过协程api介绍和核心的挂起恢复原理,再次对其设计思想进行记录,以从更上层的思维模型构筑方面了解 Kotlin 语言的协程。

已有线程为何要使用协程呢

在 JVM 生态系统中,已经有了 Thread 这个设计,对异步计算进行建模的抽象。

但是,JVM 直接映射到 OS 线程的 线程很重 。对于每个线程,OS 必须在堆栈上 分配大量上下文信息 。此外,每次计算达到 阻塞 操作时,底层线程都会暂停,JVM 必须加载另一个线程的上下文。上下文切换成本高昂,因此我们应避免在代码中使用阻塞操作。

JVM 线程上下文(Thread Context)指的是在 JVM 中每个线程所拥有的一组信息,这些信息定义了 线程在运行时的环境和状态 。包含: (1)程序计数器(Program Counter,PC):用于记录线程当前执行的字节码指令地址。当线程被暂停后恢复执行时,程序计数器能让线程知道从哪里继续执行。 (2)栈帧(Stack Frame):线程的栈内存用于存储方法调用的信息,每个方法调用都会在栈上创建一个栈帧。栈帧包含局部变量表、操作数栈、动态链接、方法返回地址等信息。 (3)线程局部存储(Thread Local Storage,TLS):允许线程拥有自己独立的变量副本,不同线程对这些变量的操作互不影响。 (4)寄存器状态:包括 CPU 寄存器的值,如通用寄存器、指令指针寄存器等。这些寄存器的值反映了线程当前的执行状态。 (5)线程优先级:决定了线程在竞争 CPU 资源时的优先顺序。 (6)线程状态:如新建、就绪、运行、阻塞、终止等状态。

另一方面,正如我们将看到的,协程非常轻量级。它们不是直接映射到操作系统线程上,而是在用户级别,使用称为 Continuation 的简单对象。在协程之间切换不需要操作系统加载另一个线程的上下文,而是切换对 Continuation 对象的引用。

采用协程的另一个很好的理由是它们是一种 以同步方式编写异步代码 的方法。

作为替代方案,我们可以使用回调。但是,回调不太优雅,而且不可组合。此外,很难推理它们。很容易陷入回调地狱,代码难以阅读和维护:

a(aInput) { resultFromA ->
  b(resultFromA) { resultFromB ->
    c(resultFromB) { resultFromC ->
      d(resultFromC) { resultFromD ->
        println("A, B, C, D: $resultFromA, $resultFromB, $resultFromC, $resultFromD")
      }
    }
  }
}

上面的例子展示了使用回调风格执行四个函数。我们可以看出,收集四个函数返回的四个值需要很多工作。而且,代码还有很多优化空间,可以变得易于阅读和维护一些。

异步编程中使用的另一种模型是响应式编程。然而,问题在于它需要生成更复杂的代码才能理解和维护。让我们以 RxJava 库官方文档中的以下代码片段为例:

Flowable.fromCallable(() -> {
    Thread.sleep(1000); //  imitate expensive computation
    return "Done";
})
  .subscribeOn(Schedulers.io())
  .observeOn(Schedulers.single())
  .subscribe(System.out::println, Throwable::printStackTrace);

上述代码模拟了在后台线程上运行某些计算和网络请求,并在 UI 线程上显示结果(或错误)。它不是自解释的,并不能立即看懂每个方法的作用是什么,我们需要熟悉该库才能理解发生了什么。

协程解决了上述所有问题。让我们看看它是如何解决的。

suspend挂起函数

首先,你可以将协程视为轻量级线程,这意味着它不直接映射到操作系统线程。它是一种可以 随时暂停和恢复 的计算任务。因此,在开始了解如何构建协程之前,我们需要了解如何暂停和恢复协程。

Kotlin 提供了 suspend 关键字来标记可以暂停协程的函数,即允许它暂停并稍后恢复:

suspend fun bathTime() {
  logger.info("Going to the bathroom")
  delay(500L)
  logger.info("Exiting the bathroom")
}

delay(timeMillis: Long) 函数是suspend函数,会暂停协程500ms。

suspend函数只能从协程或其他suspend函数调用 。它可以被暂停和恢复。在上面的例子中,bathTime函数里,当协程执行到了delay函数时,batchTime函数可以被暂停。一旦delay执行完毕,batchTime恢复,其将从暂停后立即执行的行继续执行。

上述机制完全在 Kotlin 运行时中实现,但它是如何实现的呢?无需深入研究协程的内部结构,suspend function的整个上下文保存在类型为 的对象中 Continuation<T> 。T类型变量表示函数的返回类型。

Continuation 包含函数变量和参数的所有状态。此外,它还包括 一个标签 ,用于存储执行暂停的点。因此, Kotlin 编译器将重写每个suspend function ,在函数签名中添加一个 Continuation 类型的参数。我们的函数签名bathTime将被重写如下:

fun bathTime(continuation: Continuation<*>): Any

为什么编译器还要 改变返回值类型 为 Any 呢?答案是,当函数suspend被挂起时,它不能直接返回函数的值。它必须返回一个值来标记该函数被挂起COROUTINE_SUSPENDED,这样调用方才知道自己调用了一个挂起函数,需要在这里暂停自身的执行。

在 continuation 对象内部,编译器将保存函数执行的状态。由于我们没有参数,也没有内部变量,因此 continuation 仅存储标记 执行进度的标签 。为了简单起见,我们引入一个 BathTimeContinuation 类型来存储函数的上下文。

在我们的示例中,运行时可以bathTime在函数开始时或delay函数之后调用该函数。如果我们使用Int标签,则可以表示函数的两种可能状态,如下所示:

fun bathTime(continuation: Continuation<*>): Any {
    val continuation =
      continuation as? BathTimeContinuation ?: BathTimeContinuation(continuation)
    if (continuation.label == 0) {
      logger.info("Going to the bathroom")
      continuation.label = 1
      if (delay(500L, continuation) == COROUTINE_SUSPENDED) 
          return COROUTINE_SUSPENDED
    }
    if (continuation.label == 1) {
      logger.info("Exiting the bathroom")
    }
    error("This line should never be reached")
}

首先,必须检查continuation对象是否是 BathTimeContinuation 类型。如果不是,我们创建一个新BathTimeContinuation对象,并将该continuation对象作为参数传递。

当bathTime第一次调用该函数时,我们会创建一个新的continuation实例。正如我们所见,continuation就像一层层的洋葱:每次调用suspend function时,我们都会将 continuation 对象包装在一个新的 continuation 中。

然后,如果label是0,我们打印第一条消息并将标签设置为1。然后,我们调用该delay函数,传递continuation对象。如果delay函数返回COROUTINE_SUSPENDED,则表示该函数已暂停,我们也返回COROUTINE_SUSPENDED给调用者。

假设delay函数返回的值不同于COROUTINE_SUSPENDED。在这种情况下,这意味着函数已恢复,我们可以继续执行该bathTime函数。如果标签是1,则该函数刚刚恢复,我们打印第二条消息。

以上是 Kotlin 编译器生成并由 Kotlin 运行时运行的实际代码的简化版本。不过,这足以理解协程的工作原理。

简单来说,就是 将需要执行的代码封装在Continuation对象 中,并将其传递给JVM运行时。运行时将检查该Continuation对象是否已暂停。如果是,则运行时将暂停该函数的执行,并将该待执行的Continuation对象传递给该函数,作为一种回调。进入到下一层的挂起函数中,重复这个检查,直接遇到非 COROUTINE_SUSPENDED 状态的返回值,这时候运行时将一层层地恢复该函数的执行。

协程作用域和结构并发

现在我们可以开始研究 Kotlin 如何实现结构并发的概念。让我们声明另一个suspend function,它将模拟煮沸一些水的动作:

suspend fun boilingWater() {
    logger.info("Boiling water")
    delay(1000L)
    logger.info("Water boiled")
}

我们介绍的第一个函数是 coroutineScope 挂起函数。此函数是协程的核心,用于创建新的协程作用域。它以挂起 lambda 作为参数,以 CoroutineScope 的实例作为接收者:

suspend fun <R> coroutineScope(
  block: suspend CoroutineScope.() -> R
): R

协程作用域代表了 Kotlin 中结构化并发的实现。 运行时会阻塞 lambda 的执行,block直到 lambda 内部启动的所有协程block都完成 。这些协程被称为作用域的子协程。此外,结构化并发还为我们带来了以下特性:

  • 子协程继承父协程的上下文 (CoroutineContext),并且可以覆盖它。协程的上下文是Continuation我们之前见过的对象的一部分。它包含协程的名称、调度程序(即执行协程的线程池)、异常处理程序等。
  • 当父协程被取消时,子协程也会被取消。
  • 当子协程抛出异常时,父协程也会停止。

此外,该coroutineScope函数还创建了一个新的协程,它会暂停前一个协程的执行,直到其执行结束。因此,如果我们想按顺序执行晨间例程的两个步骤,我们可以使用以下代码:

suspend fun sequentialMorningRoutine() {
  coroutineScope {
    bathTime()
  }
  // coroutineScope会挂起当前协程,等bathTime走完才会往下执行
  coroutineScope {
    boilingWater()
  }
}

为了执行sequentialMorningRoutine,我们必须声明一个暂停main函数,我们将在本文的其余部分重复使用该函数:

suspend fun main() {
    logger.info("Starting the morning routine")
    sequentialMorningRoutine()
    logger.info("Ending the morning routine")
}

该sequentialMorningRoutine函数将按顺序执行该bathTime函数,然后boilingWater在两个不同的协程中执行该函数。因此,我们不应该对上述代码的输出类似于以下内容感到惊讶:

15:27:05.260 [main] INFO CoroutinesPlayground - Starting the morning routine
15:27:05.286 [main] INFO CoroutinesPlayground - Going to the bathroom
15:27:05.811 [kotlinx.coroutines.DefaultExecutor] INFO CoroutinesPlayground - Exiting the bathroom
15:27:05.826 [kotlinx.coroutines.DefaultExecutor] INFO CoroutinesPlayground - Boiling water
15:27:06.829 [kotlinx.coroutines.DefaultExecutor] INFO CoroutinesPlayground - Water boiled
15:27:06.830 [kotlinx.coroutines.DefaultExecutor] INFO CoroutinesPlayground - Ending the morning routine

我们可以看到,执行是纯顺序的。但是,我们可以看到运行时使用两个不同的线程来执行整个过程,即 mainkotlinx.coroutines.DefaultExecutor 线程。协程的一个重要特性是, 当它们恢复时,它们可以在与暂停它们的线程不同的线程中执行 。例如,bathTime协程在 main 主线程上启动。然后,delay函数将其暂停。最后,它在 kotlinx.coroutines.DefaultExecutor 线程上恢复。

协程构建器

launch Builder

至此,我们应该了解suspend function和结构并发的基础知识。现在是时候明确创建我们的第一个协程了。Kotlin 协程库提供了一组称为 builders 的函数。这些函数用于创建协程并开始执行。我们将看到的第一个函数是launch:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

该库将 launch 构建器定义为 CoroutineScope 类型的扩展函数。因此,我们需要一个作用域来以这种方式创建协程。要创建协程,我们还需要一个CoroutineContext和一个包含要执行的代码的 lambda。构建器将把它作为接收器传递CoroutineScope给blocklambda。这样,我们可以重用作用域来创建新的子协程。最后,构建器的默认行为是立即启动新的协程(CoroutineStart.DEFAULT)。

因此,让我们在早晨的例行工作中添加一些并发功能。我们可以在两个新的协程中启动boilingWater和bathTime函数,并观察它们的竞争情况:

suspend fun concurrentMorningRoutine() {
    coroutineScope {
        launch {
            bathTime()
        }
        launch {
            boilingWater()
        }
    }
}

上述代码的日志类似于以下内容:

09:09:44.817 [main] INFO CoroutinesPlayground - Starting the morning routine
09:09:44.870 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Going to the bathroom
09:09:44.871 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Boiling water
09:09:45.380 [DefaultDispatcher-worker-2 @coroutine#1] INFO CoroutinesPlayground - Exiting the bathroom
09:09:45.875 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Water boiled
09:09:45.876 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Ending the morning routine

我们可以从上面的日志中提取出很多信息。首先,我们可以看到我们有效地产生了两个新的协程,coroutine#1和coroutine#2。第一个运行bathTime挂起函数,第二个运行boilingWater。

两个函数的日志是交错的,因此两个函数的执行是并发的。这种并发模型是协作的。只有coroutine#1遇到suspend函数,暂停执行时,coroutine#2才有机会执行。

此外,coroutine#1在线程上运行 DefaultDispatcher-worker-1 暂停,而在 DefaultDispatcher-worker-2线程上恢复。协程在可配置的线程池上运行。正如日志所建议的那样,默认线程池被称为Dispatchers.Default。

最后但并非最不重要的一点是,日志显示了结构并发的一个清晰示例。执行main在两个协程执行后打印了方法中的最后一条日志。我们可能已经注意到,我们没有任何显式同步机制来实现main函数中的这一结果。我们没有等待或延迟main函数的执行。正如我们所说,这是由于结构并发。该coroutineScope函数创建一个用于创建两个协程的作用域。由于这两个协程是同一作用域的子代,因此它将等到它们两个的执行结束才返回。

我们也可以避免使用结构化并发。在这种情况下,我们需要添加一些等待协程执行结束的操作。我们可以使用GlobalScope对象而不是 coroutineScope 函数。它就像一个空的协程作用域,不强制任何父子关系。因此,我们可以重写晨间例程函数,如下所示:

suspend fun noStructuralConcurrencyMorningRoutine() {
    GlobalScope.launch {
        bathTime()
    }
    GlobalScope.launch {
        boilingWater()
    }
    Thread.sleep(1500L)
}

上述代码的日志与前一个代码大体相同:

14:06:57.670 [main] INFO CoroutinesPlayground - Starting the morning routine
14:06:57.755 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Boiling water
14:06:57.755 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Going to the bathroom
14:06:58.264 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Exiting the bathroom
14:06:58.763 [DefaultDispatcher-worker-1 @coroutine#2] INFO CoroutinesPlayground - Water boiled
14:06:59.257 [main] INFO CoroutinesPlayground - Ending the morning routine

由于我们没有使用任何结构化的并发机制GlobalScope,我们Thread.sleep(1500L)在函数末尾添加了一个,以等待两个协程的执行结束。如果我们删除该Thread.sleep调用,日志将类似于以下内容:

21:47:09.418 [main] INFO CoroutinesPlayground - Starting the morning routine
21:47:09.506 [main] INFO CoroutinesPlayground - Ending the morning routine

正如预期的那样,主函数在两个协程执行结束之前就返回了。因此,我们可以说,GlobalScope不是创建协程的好选择

如果我们查看该 launch 函数的定义,我们可以看到它返回一个Job对象。该对象是 coroutine 的句柄。我们可以使用它来取消协程的执行或等待其完成。让我们看看如何使用它来等待协程的完成。让我们为我们的钱包添加一个新的suspend function:

suspend fun preparingCoffee() {
    logger.info("Preparing coffee")
    delay(500L)
    logger.info("Coffee prepared")
}

在我们的早晨例行工作中,我们只想在洗澡和烧水后准备咖啡。因此,我们需要等待两个协程的完成。我们可以通过join在结果Job对象上调用方法来做到这一点,join方法是一个suspend函数,可以用于等待协程的block完全执行完毕,代码如下:

suspend fun morningRoutineWithCoffee() {
    coroutineScope {
        val bathTimeJob: Job = launch {
            bathTime()
        }
        val  boilingWaterJob: Job = launch {
            boilingWater()
        }
        bathTimeJob.join()
        boilingWaterJob.join()
        launch {
            preparingCoffee()
        }
    }
}

正如我们所料,从日志中我们可以看到,在两个协程执行结束后,我们才准备了咖啡:

21:56:18.040 [main] INFO CoroutinesPlayground - Starting the morning routine
21:56:18.128 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Going to the bathroom
21:56:18.130 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Boiling water
21:56:18.639 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Exiting the bathroom
21:56:19.136 [DefaultDispatcher-worker-1 @coroutine#2] INFO CoroutinesPlayground - Water boiled
21:56:19.234 [DefaultDispatcher-worker-2 @coroutine#3] INFO CoroutinesPlayground - Preparing coffee
21:56:19.739 [DefaultDispatcher-worker-2 @coroutine#3] INFO CoroutinesPlayground - Coffee prepared
21:56:19.739 [DefaultDispatcher-worker-2 @coroutine#3] INFO CoroutinesPlayground - Ending the morning routine

但是,既然我们现在知道了结构并发的所有秘密,我们可以 使用coroutineScope函数 的功能重写上述代码:

suspend fun structuralConcurrentMorningRoutineWithCoffee() {
    coroutineScope {
        coroutineScope {
            launch {
                bathTime()
            }
            launch {
                boilingWater()
            }
        }
        launch {
            preparingCoffee()
        }
    }
}

上述代码的输出和前一个代码相同。

async Builder

如果我们想从协程的执行中返回一个值怎么办?例如,让我们定义两个新的挂起函数:前者产生我们准备的咖啡混合物。同时,后者返回烤面包:

suspend fun preparingJavaCoffee(): String {
  logger.info("Preparing coffee")
  delay(500L)
  logger.info("Coffee prepared")
  return "Java coffee"
}

suspend fun toastingBread(): String {
  logger.info("Toasting bread")
  delay(1000L)
  logger.info("Bread toasted")
  return "Toasted bread"
}

幸运的是,库提供了一种让协程返回值的方法。我们可以使用 async构建器 创建一个返回值的协程。具体来说,它会产生一个 Deferred<T> 类型的值,其行为或多或少类似于 java Future<T> 。在 Deferred<T> 类型的对象上,我们可以调用 await方法 等待协程完成并获取返回值。库还将async构建器定义为 CoroutineScope 扩展方法:

public fun <T> CoroutineScope.async(
  context: CoroutineContext = EmptyCoroutineContext,
  start: CoroutineStart = CoroutineStart.DEFAULT,
  block: suspend CoroutineScope.() -> T
): Deferred<T>

让我们看看如何使用它来返回我们准备的咖啡和烤面包的混合:

suspend fun breakfastPreparation() {
    coroutineScope {
        val coffee: Deferred<String> = async {
            preparingJavaCoffee()
        }
        val toast: Deferred<String> = async {
            toastingBread()
        }
        logger.info("I'm eating ${coffee.await()} and ${toast.await()}")
    }
}

如果我们查看日志,我们可以看到两个协程的执行仍然是并发的。最后一条日志等待两个协程完成后打印,最终消息:

21:56:46.091 [main] INFO CoroutinesPlayground - Starting the morning routine
21:56:46.253 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Preparing coffee
21:56:46.258 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Toasting bread
21:56:46.758 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Coffee prepared
21:56:47.263 [DefaultDispatcher-worker-1 @coroutine#2] INFO CoroutinesPlayground - Bread toasted
21:56:47.263 [DefaultDispatcher-worker-1 @coroutine#2] INFO CoroutinesPlayground - I'm eating Java coffee and Toasted bread
21:56:47.263 [DefaultDispatcher-worker-1 @coroutine#2] INFO CoroutinesPlayground - Ending the morning routine

协作调度

到这里,我们应该对协程的一些基础知识有所了解了。然而,我们仍然需要讨论协程的一个重要方面:协作调度。

协程调度模型与 Java 采用的Threads抢占式调度模型有很大不同。在抢占式调度中,操作系统决定何时从一个线程切换到另一个线程。在协作式调度中, 协程本身决定何时将控制权交给另一个协程

在 Kotlin 中,协程决定放弃控制权并到达挂起函数。只有此时执行它的线程才会被释放并允许运行另一个协程。

如果我们注意到,在迄今为止看到的日志中,执行控制在调用delay挂起函数时总是会发生变化。但是,为了更好地理解它,让我们看另一个示例。让我们定义一个新的挂起函数来模拟执行一个非常长时间运行的任务:

suspend fun workingHard() {
    logger.info("Working")
    while (true) {
        // Do nothing
    }
    delay(100L)
    logger.info("Work done")
}

无限循环会阻止函数到达delay挂起函数,因此协程永远不会放弃控制权。现在,我们定义另一个挂起函数与前一个函数并发执行:

suspend fun takeABreak() {
    logger.info("Taking a break")
    delay(1000L)
    logger.info("Break done")
}

最后,让我们将所有内容整合到一个新的挂起函数中,该函数在两个专用协程中运行前两个函数。为了确保我们能看到协作调度的效果,我们将执行协程的线程池限制为单个线程:

@OptIn(ExperimentalCoroutinesApi::class)
suspend fun workingHardRoutine() {
  val dispatcher: CoroutineDispatcher = Dispatchers.Default.limitedParallelism(1)
  coroutineScope {
    launch(dispatcher) {
      workingHard()
    }
    launch(dispatcher) {
      takeABreak()
    }
  }
}

表示CoroutineDispatcher用于执行协程的线程池。该limitedParallelism函数是接口的扩展方法CoroutineDispatcher,用于 将线程池中的线程数限制为给定值 。由于这是一个实验性 API,因此我们需要用@OptIn(ExperimentalCoroutinesApi::class)注释注释该函数以避免编译器警告。

我们在唯一可用的线程上启动了两个协程dispatcher,日志向我们展示了协作调度的效果:

08:46:04.804 [main] INFO CoroutinesPlayground - Starting the morning routine
08:46:04.884 [DefaultDispatcher-worker-2 @coroutine#1] INFO CoroutinesPlayground - Working
-- Running forever --

由于workingHard协程从未到达挂起函数,因此它永远不会交出控制权。然后,takeABreak协程永远不会被执行。相反,如果我们定义一个挂起函数,将控制权交还给调度程序,takeABreak协程将有机会被执行:

suspend fun workingConsciousness() {
    logger.info("Working")
    while (true) {
        delay(100L)
    }
    logger.info("Work done")
}

@OptIn(ExperimentalCoroutinesApi::class)
suspend fun workingConsciousnessRoutine() {
  val dispatcher: CoroutineDispatcher = Dispatchers.Default.limitedParallelism(1)
  coroutineScope {
    launch(dispatcher) {
      workingConsciousness()
    }
    launch(dispatcher) {
      takeABreak()
    }
  }
}

现在,日志显示takeABreak协程 有机会执行 ,即使 workingConsciousness 永远运行,并且我们只有一个线程:

09:02:49.302 [main] INFO CoroutinesPlayground - Starting the morning routine
09:02:49.376 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Working
09:02:49.382 [DefaultDispatcher-worker-1 @coroutine#2] INFO CoroutinesPlayground - Taking a break
09:02:50.387 [DefaultDispatcher-worker-1 @coroutine#2] INFO CoroutinesPlayground - Break done
-- Running forever --

我们可以使用协程来获取相同的日志workingHard,并向线程池中添加一个线程:

@OptIn(ExperimentalCoroutinesApi::class)
suspend fun workingHardRoutine() {
  val dispatcher: CoroutineDispatcher = Dispatchers.Default.limitedParallelism(2)
  coroutineScope {
    launch(dispatcher) {
      workingHard()
    }
    launch(dispatcher) {
      takeABreak()
    }
  }
}

由于我们有两个线程和两个协程,因此并发度现在为 2。照例,日志证实了该理论:coroutine#1在 上执行DefaultDispatcher-worker-1,coroutine#2在 上执行DefaultDispatcher-worker-2。

13:40:59.864 [main] INFO CoroutinesPlayground - Starting the morning routine
13:40:59.998 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Working
13:41:00.003 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Taking a break
13:41:01.010 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Break done
-- Running forever --

协作式调度迫使我们在设计协程时非常小心。假设一个协程执行了一个阻塞底层线程的操作,比如 JDBC 调用。在这种情况下,它会阻止该线程执行任何其他协程。

因此,该库允许我们针对不同的操作使用不同的调度程序。主要有:

  • Dispatchers.Default是库使用的默认调度程序。它使用线程数等于可用处理器数的线程池。它是 CPU 密集型操作的正确选择。
  • Dispatchers.IO是用于 I/O 操作的调度程序。它使用线程池,线程数等于可用处理器数,或最多 64 个。它是 I/O 操作(例如网络调用或文件操作)的正确选择。
  • 从线程池创建的 Dispatcher:可以CoroutineDispatcher使用线程池来创建我们的实例。我们可以轻松使用接口asCoroutineDispatcher的扩展功能Executor。但是,请注意,当我们不再需要底层线程池时,我们有责任将其关闭:val dispatcher = Executors.newFixedThreadPool(10).asCoroutineDispatcher()

如果我们同时拥有 CPU 密集型部分和阻塞部分,我们必须同时使用Dispatchers.DefaultDispatchers.IO ,并确保在默认调度程序上启动 CPU 密集型协程,在 IO 调度程序上启动阻塞代码。

协程的取消

当我们思考并发编程时,取消始终是一个棘手的话题。终止线程并突然停止任务的执行并不是一个好的做法。在停止任务之前,我们必须释放正在使用的资源,避免泄漏,并使系统处于一致状态。

我们可以想象,Kotlin 允许我们取消协程的执行。该库提供了一种机制来协作取消协程以避免出现问题。该Job类型提供了一个cancel取消协程执行的函数。但是,取消不是立即的,只有当协程到达暂停点时才会发生。该机制与我们在协作调度中看到的机制非常接近。

让我们看一个例子。我们想模拟一下我们在工作期间接到一个重要电话。我们忘记了我们最好的朋友的生日,我们想在商场关门前去买一份礼物:

suspend fun forgettingTheBirthDayRoutine() {
  coroutineScope {
    val workingJob = launch {
      workingConsciousness()
    }
    launch {
      delay(2000L)
      workingJob.cancel()
      workingJob.join()
      logger.info("I forgot the birthday! Let's go to the mall!")
    }
  }
}

此代码片段中发生了很多事情。首先,我们启动了workingConsciousness协程并收集了相应的Job。我们使用了workingConsciousness挂起函数,因为它在无限循环内挂起,并调用该delay函数。

同时,我们启动另一个协程,该协程workingJob在 2 秒后调用 workingJob 的取消函数,并等待其完成。workingJob被取消,但workingConsciousness协程不会立即停止。它继续执行,直到 到达暂停点 ,然后被取消。由于我们想等待取消,我们调用了workingJob的join函数。

日志证实了这一理论。在 coroutine#1 启动后约 2 秒,coroutine#2打印了其日志,并且coroutine#1被取消:

21:36:04.205 [main] INFO CoroutinesPlayground - Starting the morning routine
21:36:04.278 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Working
21:36:06.390 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - I forgot the birthday! Let's go to the mall!
21:36:06.391 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Ending the morning routine

cancel和join配合使用的模式非常常见,因此 Kotlin 协程库为我们提供了一个cancelAndJoin结合这两种操作的函数。

正如我们所说,在 Kotlin 中,取消是一种合作行为。 如果协程从不暂停,则根本无法取消 。让我们改用suspend function来更改上述示例workingHard。在这种情况下,该workingHard函数从不暂停,因此我们预计workingJob无法取消:

suspend fun forgettingTheBirthDayRoutineWhileWorkingHard() {
    coroutineScope {
        val workingJob = launch {
            workingHard()
        }
        launch {
            delay(2000L)
            workingJob.cancelAndJoin()
            logger.info("I forgot the birthday! Let's go to the mall!")
        }
    }
}

这次,我们的朋友将不会收到她的礼物。workingJob被取消,但workingHard函数没有停止,因为它从未到达暂停点。 日志再次证实了这一理论:

08:56:10.784 [main] INFO CoroutinesPlayground - Starting the morning routine
08:56:10.849 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Working
-- Running forever --

在后台,该cancel函数将 设置Job为“正在取消”状态。在第一次到达暂停点时,运行时抛出一个CancellationException,协程最终被取消。这种机制使我们能够安全地清理协程使用的资源。我们可以实施许多策略来清理资源,但首先,我们需要在示例中释放资源。我们可以定义代表我们办公室办公桌的 Desk 类:

class Desk : AutoCloseable {
    init {
        logger.info("Starting to work on the desk")
    }

    override fun close() {
        logger.info("Cleaning the desk")
    }
}

该类Desk实现了AutoCloseable接口。因此,它是协程取消期间释放资源的绝佳选择。由于它实现了AutoCloseable,我们可以使用该use函数在代码块完成时自动关闭资源:

suspend fun forgettingTheBirthDayRoutineAndCleaningTheDesk() {
    val desk = Desk()
    coroutineScope {
        val workingJob = launch {
            desk.use { _ ->
                workingConsciousness()
            }
        }
        launch {
            delay(2000L)
            workingJob.cancelAndJoin()
            logger.info("I forgot the birthday! Let's go to the mall!")
        }
    }
}

use 是 Kotlin 标准库中的一个扩展函数,主要用于自动管理需要关闭的资源(如文件、网络连接等)。它确保资源在使用完毕后被正确关闭,即使发生异常也不会遗漏。

正如预期的那样,在我们搬到商场之前,我们清理了桌子,日志也证实了这一点:

21:38:30.117 [main] INFO CoroutinesPlayground - Starting the morning routine
21:38:30.124 [main] INFO CoroutinesPlayground - Starting to work on the desk
21:38:30.226 [DefaultDispatcher-worker-1 @coroutine#1] INFO CoroutinesPlayground - Working
21:38:32.298 [DefaultDispatcher-worker-2 @coroutine#1] INFO CoroutinesPlayground - Cleaning the desk
21:38:32.298 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - I forgot the birthday! Let's go to the mall!
21:38:32.298 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Ending the morning routine

我们还可以使用invokeOnCompletion取消上的函数在函数完成workingConsciousness Job后清理桌面:

suspend fun forgettingTheBirthDayRoutineAndCleaningTheDeskOnCompletion() {
  val desk = Desk()
  coroutineScope {
    val workingJob = launch {
      workingConsciousness()
    }
    workingJob.invokeOnCompletion { exception: Throwable? ->
      desk.close()
    }
    launch {
      delay(2000L)
      workingJob.cancelAndJoin()
      logger.info("I forgot the birthday! Let's go to the mall!")
    }
  }
}

我们可以看到,该invokeOnCompletion方法将可空异常作为输入参数。如果Job被取消,则异常为CancellationException。

取消的另一个特性是它会传播到子协程。 当我们取消一个协程时,我们会隐式取消它的所有子协程 。让我们看一个例子。白天,保持水分是必不可少的。我们可以使用 drinkWater 来喝水:

suspend fun drinkWater() {
  while (true) {
    logger.info("Drinking water")
    delay(1000L)
    logger.info("Water drunk")
  }
}

然后,我们可以创建一个协程,并生成两个新的协程,分别用于工作和饮用水。最后,我们可以取消父协程,并且我们期望两个子协程也被取消:

suspend fun forgettingTheBirthDayWhileWorkingAndDrinkingWaterRoutine() {
    coroutineScope {
        val workingJob = launch {
            launch {
                workingConsciousness()
            }
            launch {
                drinkWater()
            }
        }
        launch {
            delay(2000L)
            workingJob.cancelAndJoin()
            logger.info("I forgot the birthday! Let's go to the mall!")
        }
    }
}

正如预期的那样,当我们取消 时workingJob,我们也会取消并停止其子协程。以下是描述情况的日志:

13:18:49.143 [main] INFO CoroutinesPlayground - Starting the morning routine
13:18:49.275 [DefaultDispatcher-worker-2 @coroutine#2] INFO CoroutinesPlayground - Working
13:18:49.285 [DefaultDispatcher-worker-3 @coroutine#3] INFO CoroutinesPlayground - Drinking water
13:18:50.285 [DefaultDispatcher-worker-3 @coroutine#3] INFO CoroutinesPlayground - Water drunk
13:18:50.286 [DefaultDispatcher-worker-3 @coroutine#3] INFO CoroutinesPlayground - Drinking water
13:18:51.288 [DefaultDispatcher-worker-2 @coroutine#3] INFO CoroutinesPlayground - Water drunk
13:18:51.288 [DefaultDispatcher-worker-2 @coroutine#3] INFO CoroutinesPlayground - Drinking water
13:18:51.357 [DefaultDispatcher-worker-2 @coroutine#4] INFO CoroutinesPlayground - I forgot the birthday! Let's go to the mall!
13:18:51.357 [DefaultDispatcher-worker-2 @coroutine#4] INFO CoroutinesPlayground - Ending the morning routine

这就是协程取消的全部内容!

协程上下文

在关于continuation的部分和关于构建器的部分中,我们简要介绍了协程上下文的概念。此外,CoroutineScope保留对协程上下文的引用。你可以想象, 这是一种存储从父级传递给子级的信息的方法 ,以在内部开发结构并发性。

表示协程上下文的类型称为CoroutineContext,它是 Kotlin 核心库的一部分。这是一个有趣的类型,因为它表示元素的集合,但同时,每个元素都是一个集合:

public interface CoroutineContext
// But also
public interface Element : CoroutineContext

CoroutineContext 的实现与 Continuation<T> 类型一起放在 Kotlin 协程库中。在实际实现中,我们有CoroutineName,它代表协程的名称:

val name: CoroutineContext = CoroutineName("Morning Routine")

此外,CoroutineDispatcher和Job类型实现了CoroutineContext接口。我们在上面的日志中看到的标识符是CoroutineId。当我们启用调试模式时,运行时会自动将此上下文添加到每个协程中。

由于 CoroutineContext 其行为类似于集合,因此该库还定义了向上下文添加元素的 + 运算符。因此,创建一个包含许多元素的新上下文非常简单:

val context: CoroutineContext = CoroutineName("Morning Routine") + Dispatchers.Default + Job()

也可以使用以下函数从上下文中删除元素minusKey:

val newContext: CoroutineContext = context.minusKey(CoroutineName)

我们应该记住,我们可以将上下文传递给构建器来更改所创建协程的行为。例如,假设我们想要创建一个使用 Dispatchers.Default 的特定名称的协程。在这种情况下,我们可以按如下方式执行:

suspend fun asynchronousGreeting() {
    coroutineScope {
        launch(CoroutineName("Greeting Coroutine") + Dispatchers.Default) {
            logger.info("Hello Everyone!")
        }
    }
}

我们在main函数内部运行一下,在日志中我们可以看到,这个协程以指定的名称创建,并在调度器中执行Default:

11:56:46.747 [DefaultDispatcher-worker-1 @Greeting Coroutine#1] INFO CoroutinesPlayground - Hello Everyone!

协程上下文也可以表现得像一个映射,因为我们可以使用与我们要检索的元素相对应的类型的名称来搜索和访问它包含的元素:

logger.info("Coroutine name: {}", context[CoroutineName]?.name)

上述代码打印了上下文中存储的协程名称(如果有)。CoroutineName方括号内的既不是类型也不是类。实际上,它引用了Key类的伴生对象,即只是一些 Kotlin 语法糖。

该库还定义了 EmptyCoroutineContext 空的协程上下文,我们可以将其用作“零”元素来创建新的自定义上下文。

因此,上下文是一种在协程之间传递信息的方式。任何父协程都会将其上下文提供给其子协程。子协程将值从父级复制到它们可以覆盖的上下文的新实例。让我们看一个没有覆盖的继承示例:

suspend fun coroutineCtxInheritance() {
    coroutineScope {
        launch(CoroutineName("Greeting Coroutine")) {
            logger.info("Hello everyone from the outer coroutine!")
            launch {
                logger.info("Hello everyone from the inner coroutine!")
            }
            delay(200L)
            logger.info("Hello again from the outer coroutine!")
        }
    }
}

上述代码的日志如下,它突出显示两个协程共享相同的名称:

12:19:12.962 [DefaultDispatcher-worker-1 @Greeting Coroutine#1] INFO CoroutinesPlayground - Hello everyone from the outer coroutine!
12:19:12.963 [DefaultDispatcher-worker-2 @Greeting Coroutine#2] INFO CoroutinesPlayground - Hello everyone from the inner coroutine!
12:19:12.963 [DefaultDispatcher-worker-1 @Greeting Coroutine#1] INFO CoroutinesPlayground - Hello again from the outer coroutine!

正如我们所说的,如果我们愿意,我们可以从子协程覆盖上下文中的值:

suspend fun coroutineCtxOverride() {
    coroutineScope {
        launch(CoroutineName("Greeting Coroutine")) {
            logger.info("Hello everyone from the outer coroutine!")
            launch(CoroutineName("Greeting Inner Coroutine")) {
                logger.info("Hello everyone from the inner coroutine!")
            }
            delay(200L)
            logger.info("Hello again from the outer coroutine!")
        }
    }
}

上面代码的log显示了父协程被覆盖了,但是父上下文中的值还是原来的:

12:22:33.869 [DefaultDispatcher-worker-1 @Greeting Coroutine#1] INFO CoroutinesPlayground - Hello everyone from the outer coroutine!
12:22:33.870 [DefaultDispatcher-worker-2 @Greeting Inner Coroutine#2] INFO CoroutinesPlayground - Hello everyone from the inner coroutine!
12:22:34.077 [DefaultDispatcher-worker-1 @Greeting Coroutine#1] INFO CoroutinesPlayground - Hello again from the outer coroutine!
12:22:34.078 [DefaultDispatcher-worker-1 @Greeting Coroutine#1] INFO CoroutinesPlayground - Ending the morning routine

上下文继承规则的唯一例外是Job上下文实例。每个新协程都会创建自己的Job实例,该实例不会从父级继承。而其他上下文元素(例如CoroutineName或调度程序)则从父级继承。

【Kotlin】协程的挂起恢复源码解析

【Kotlin】协程的挂起恢复源码解析

本文介绍了CPS转换,后从反编译的源码层面介绍Kotlin协程挂起和恢复的原理

首先要了解的就是CPS转换。

CPS转换

在Kotlin协程中,挂起函数的执行是通过 Continuation Passing Style (CPS)转换 来实现的。CPS转换是一种将函数式编程中的函数调用转换为可传递的 Continuation 对象的过程。这里的转换是Kotlin编译器实现的,在跨平台属性上,也保证了流程的一致性。

CPS转换调用的过程如下:

  1. 当一个函数被调用时,它的参数和返回值会被封装在一个Continuation对象中。
  2. 函数的执行过程中,遇到挂起操作时,会将当前的Continuation对象传递给挂起函数。
  3. 挂起函数执行完毕后,会将结果封装在一个新的Continuation对象中,并将其传递给原始的Continuation对象。
  4. 原始的Continuation对象会继续执行,直到所有的挂起操作都完成。

假设我们有一个简单的suspend函数,它模拟了一个异步操作:

suspend fun fetchData(): String {
    delay(1000) // 模拟耗时操作
    return "Data fetched"
}

在CPS转换后,这个函数可能会被转换为类似以下的形式:

fun fetchData(continuation: Continuation<String>) {
    delay(1000, object : Continuation<Unit> {
        override val context: CoroutineContext = continuation.context

        override fun resumeWith(result: Result<Unit>) {
            continuation.resume("Data fetched")
        }
    })
}

在这个转换后的函数中,fetchData不再直接返回结果,而是通过continuation.resume方法将结果传递给调用者。简单来说,CPS其实就是函数通过回调传递结果的一种方式。

Kotlin协程通过将异步流程拆解为一系列 挂起点 ,对含有 suspend 关键字的函数进行了 CPS转换 ,即Continuation Passing Style转换,使其能够 接收Continuation对象 作为参数,并在异步操作完成后通过调用 Continuation 的恢复方法来继续执行协程。

在编译后的字节码中,协程的状态会被转换为状态机的形式,每个挂起点对应状态机的一个状态。当协程挂起时,它的执行状态会被保存在Continuation对象中,包括局部变量上下文和执行位置。

Continuation

Continuation (续体)是一个保存协程状态的对象,它记录了协程挂起的位置以及局部变量上下文,使得协程可以在任何时候从上次挂起的地方继续执行。Continuation是一个接口,它定义了 resumeWith 方法,用于恢复协程的执行。

interface Continuation<in T> {
   val context: CoroutineContext
   fun resumeWith(result: Result<T>)//result 为返回的结果
}
  1. 续体是一个较为抽象的概念,简单来说它包装了协程在挂起之后应该继续执行的代码;在编译的过程中,一个完整的协程被分割切块成一个又一个续体。
  2. 在suspend函数或者 await 函数的挂起结束以后,它会调用 continuation 参数的 resumeWith 函数,来恢复执行suspend函数或者await 函数后面的代码。

CPS转换 使得协程能够在不阻塞线程的情况下执行异步操作。当协程挂起时,线程可以被释放去执行其他任务,从而提高了系统的并发性能。此外,CPS转换使得协程的挂起和恢复操作对开发者来说是透明的,开发者可以像编写同步代码一样编写异步代码。

发生 CPS 变换的函数,返回值类型变成了 Any?,这是因为这个函数在发生变换后,除了要返回它本身的返回值,还要返回一个标记CoroutineSingletons.COROUTINE_SUSPENDED,为了适配各种可能性,CPS 转换后的函数返回值类型就只能是 Any?了。

协程的启动

下面跟随启动,挂起,恢复的流程,从源码层面看看协程的核心原理。

测试代码入口:

object CoroutineExample {
    private val TAG: String = "CoroutineExample"

    fun main(){
        // 启动协程,分析入口
        GlobalScope.launch(Dispatchers.Main) {
            request()
        }
    }
    private suspend fun request(): String {
        delay(2000)
        Log.e(TAG, "request complete")
        return "result from request"
    }
}

从 CoroutineScope.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
}
  • 参数一context:协程上下文,并不是我们平时理解的Android中的上下文,它是一种key-value数据结构。可以传入Main用于主线程调度。
  • 参数二start:启动模式,此处我们没有传值则为默认值(DEFAULT),共有三种启动模式。
    • DEFAULT:默认模式,创建即启动协程,可随时取消;
    • ATOMIC:自动模式,创建即启动协程,启动前不可取消;
    • LAZY:延迟启动模式,只有当调用start方法时才能启动。
  • 参数三block:协程真正执行的代码块,即上面例子中launch{}闭包内的代码块。

SuspendLambda

CoroutineScope.launch中第三个参数类型为suspend CoroutineScope.() -> Unit函数,这是怎么来的呢?我们编写代码的时候并没有这个东西,其实它由编译器生成的,我们的 block代码块 经过编译器编译后会生成一个 继承Continuation类SuspendLambda 。一起看下反编译的java代码,为了关注主要逻辑方便理解,去掉了一些无关代码大概代码如下:

public final void main() {
      BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)Dispatchers.getMain(), (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
         int label;

         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch (this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  CoroutineExample var10000 = CoroutineExample.this;
                  this.label = 1;
                  if (var10000.request(this) == var2) {
                     return var2;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            return Unit.INSTANCE;
         }
        
        ···
 }

从上面反编译的java代码中好像并不能很好的看出来协程中的block代码块具体编译长什么样子,但可以确定他是编译成了 Continuation类 ,因为我们可以看到实现的 invokeSuspend 方法实际是来自BaseContinuationImpl,而BaseContinuationImpl的父类就是Continuation。这个继承关系我们后面再说。既然从反编译的java代码中看的不明显,我们直接看上面例子的字节码文件,其中可以很明显的看到这样一段代码:

final class com/imile/pda/CoroutineExample$main$1 extends kotlin/coroutines/jvm/internal/SuspendLambda implements kotlin/jvm/functions/Function2

这下恍然大悟,launch函数的第三个参数,即协程中的 block代码块 是一个编译后 继承了SuspendLambda并且实现了Function2的实例

SuspendLambda 本质上是一个 Continuation ,前面我们已经说过 Continuation 是一个有着恢复操作的接口,其 resume 方法可以恢复协程的执行。

SuspendLambda继承机构如下:

- Continuation: 续体,恢复协程的执行
    - BaseContinuationImpl: 实现 resumeWith(Result) 方法,控制状态机的执行,定义了 invokeSuspend 抽象方法
        - ContinuationImpl: 增加 intercepted 拦截器,实现线程调度等
            - SuspendLambda: 封装协程体代码块
                - 协程体代码块生成的子类: 实现 invokeSuspend 方法,其内实现状态机流转逻辑

每一层封装都对应添加了不同的功能,我们先忽略掉这些功能细节,着眼于我们的主线,继续跟进 launch 函数执行过程,由于第二个参数是默认值(DEFAULT),所以创建的是 StandaloneCoroutine , 最后启动协程:

 // 启动协程
coroutine.start(start, coroutine, block)

// 启动协程
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
    start(block, receiver, this)
}

上面 coroutine.start 的调用涉及到运算符重载,实际上会调到 CoroutineStart.invoke() 方法:

public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
    when (this) {
        DEFAULT -> block.startCoroutineCancellable(receiver, completion)
        ATOMIC -> block.startCoroutine(receiver, completion)
        UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
        LAZY -> Unit // will start lazily
    }

这里启动方式为默认的 DEFAULT ,所以接着往下看:

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
    receiver: R, completion: Continuation<T>,
    onCancellation: ((cause: Throwable) -> Unit)? = null
) = runSafely(completion) {
    createCoroutineUnintercepted(receiver, completion)
        .intercepted()
        .resumeCancellableWith(Result.success(Unit), onCancellation)
}

整理下调用链如下:

coroutine.start(start, coroutine, block)
-> CoroutineStart.start(block, receiver, this)
-> CoroutineStart.invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>)
->  block.startCoroutineCancellable(receiver, completion)
-> 
createCoroutineUnintercepted(receiver,completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)

最后走到 createCoroutineUnintercepted(receiver,completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation) ,这里创建了一个协程,并链式调用 intercepted、resumeCancellable 方法,利用协程上下文中的 ContinuationInterceptor 对协程的执行进行拦截,intercepted 实际上调用的是 ContinuationImpl 的 intercepted 方法:

internal abstract class ContinuationImpl(
    completion: Continuation<Any?>?,
    private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
  ...
    public fun intercepted(): Continuation<Any?> =
        intercepted
            ?:(context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }
  ...
}

context[ContinuationInterceptor]?.interceptContinuation调用的是 CoroutineDispatcher 的 interceptContinuation 方法:

public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
    DispatchedContinuation(this, continuation)

内部创建了一个 DispatchedContinuation 可分发的协程实例,我们继续进到看resumeCancellableWith 方法:

internal class DispatchedContinuation<in T>(
    @JvmField val dispatcher: CoroutineDispatcher,
    @JvmField val continuation: Continuation<T>
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
  ...
  
  public fun <T> Continuation<T>.resumeCancellableWith(
    result: Result<T>,
    onCancellation: ((cause: Throwable) -> Unit)? = null
): Unit = when (this) {
  // 判断是否是DispatchedContinuation 根据我们前面的代码追踪 这里是DispatchedContinuation
    is DispatchedContinuation -> resumeCancellableWith(result, onCancellation)
    else -> resumeWith(result)
}

inline fun resumeCancellableWith(
        result: Result<T>,
        noinline onCancellation: ((cause: Throwable) -> Unit)?
    ) {
        val state = result.toState(onCancellation)
    // 判断是否需要线程调度 
   // 由于我们之前使用的是 `GlobalScope.launch(Main)` Android主线程调度器所以这里为true     
        if (dispatcher.isDispatchNeeded(context)) {
            _state = state
            resumeMode = MODE_CANCELLABLE
            dispatcher.dispatch(context, this)
        } else {
            executeUnconfined(state, MODE_CANCELLABLE) {
                if (!resumeCancelled(state)) {
                    resumeUndispatchedWith(result)
                }
            }
        }
}
  
  ...
}

最终走到 dispatcher.dispatch(context, this) 而这里的 dispatcher 就是通过工厂方法创建的 HandlerDispatcher ,dispatch() 函数第二个参数this是一个runnable这里为 DispatchedTask

HandlerDispatcher

internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
  ...
  
   //  最终执行这里的 dispatch方法 而handler则是android中的 MainHandler
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        if (!handler.post(block)) {
            cancelOnRejection(context, block)
        }
    }
  
  ...
}

这里借用 Android 的主线程消息队列来在主线程中执行 block Runnable而这个 Runnable 即为 DispatchedTask:

internal abstract class DispatchedTask<in T>(
    @JvmField public var resumeMode: Int
) : SchedulerTask() {
  ...
 public final override fun run() {
            ...
            withContinuationContext(continuation, delegate.countOrElement) {
                 ...
                if (job != null && !job.isActive) {
                    val cause = job.getCancellationException()
                    cancelCompletedResult(state, cause)
                    // 异常情况下
                    continuation.resumeWithStackTrace(cause)
                } else {
                    if (exception != null) {
                       // 异常情况下
                       continuation.resumeWithException(exception)
                    } else {
                      // step1:正常情况下走到这一步
                       continuation.resume(getSuccessfulResult(state))
                    }
                }
            }
           ...
   }
}

//step2:这是Continuation的扩展函数,内部调用了resumeWith()
@InlineOnly public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))


//step3:最终会调用到BaseContinuationImpl的resumeWith()方法中
internal abstract class BaseContinuationImpl(...) {
    // 实现 Continuation 的 resumeWith,并且是 final 的,不可被重写
    public final override fun resumeWith(result: Result<Any?>) {
        ...
        val outcome = invokeSuspend(param)
        ...
    }
    // 由编译生成的协程相关类来实现,例如 CoroutineExample$main$1
    protected abstract fun invokeSuspend(result: Result<Any?>): Any?
}

最终调用到 continuation.resumeWith() 而 resumeWith() 中会调用 invokeSuspend,即之前编译器生成的 SuspendLambda 中的 invokeSuspend 方法:

 @Nullable
     public final Object invokeSuspend(@NotNull Object $result) {
            Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch (this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  CoroutineExample var10000 = CoroutineExample.this;
                  this.label = 1;
                  if (var10000.request(this) == var2) {
                     return var2;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  break;
      }
 }

这段代码是一个状态机机制,每一个挂起点都是一种状态,协程恢复只是跳转到下一个状态,挂起点将执行过程分割成多个片段,利用状态机的机制保证各个片段按顺序执行。

可以看到 协程非阻塞的异步底层实现其实就是一种Callback回调 (这一点我们在介绍Continuation时有提到过),只不过有多个挂起点时就会有多个Callback回调,这里协程把多个Callback回调封装成了一个状态机。

以上就是协程的启动过程,下面我们再来看下协程中的重点挂起和恢复。

协程的挂起与恢复

协程的挂起和恢复有两个关键方法 : invokeSuspend() 和 resumeWith(Result)。我们以上一节中的例子,反编译后逆向剖析协程的挂起和恢复,先整体看下是怎样的一个过程。

suspend fun reqeust(): String {
    delay(2000)
    return "result from request"
}

反编译后的代码如下(为了方便理解,代码有删减和修改):

//1.函数返回值由String变成Object,入参也增加了Continuation参数
public final Object reqeust(@NotNull Continuation completion) {
   //2.通过completion创建一个ContinuationImpl,并且复写了invokeSuspend()
   Object continuation;
   if (completion instanceof <undefinedtype>){
     continuation =  <undefinedtype>completion
   }else{
     continuation = new ContinuationImpl(completion) {
       Object result;
       int label; //初始值为0
        
       @Nullable
       public final Object invokeSuspend(@NotNull Object $result) {
          this.result = $result;
          this.label |= Integer.MIN_VALUE;
          return request(this);//又调用了request()方法
       }
    };
  }

   Object $result = (continuation).result;
   Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
   //状态机  
   //3.方法被恢复的时候又会走到这里,第一次进入case 0分支,label的值从0变为1,第二次进入就会走case 1分支
   switch(continuation.label) {
       case 0:
          ResultKt.throwOnFailure($result);
          continuation.label = 1;
          //4.delay()方法被suspend修饰,传入一个continuation回调,返回一个object结果。这个结果要么是`COROUTINE_SUSPENDED`,否则就是真实结果。
          Object delay = DelayKt.delay(2000L, continuation)
          if (delay == var4) {
            //如果是 COROUTINE_SUSPENDED 则直接return,就不会往下执行了,request()被暂停了。
            // 如果不是COROUTINE_SUSPENDED,则说明不需要挂起,就会break跳出switch语句。正常返回继续往下执行后续外部代码。
             return var4;
          }
          break;
       case 1:
          ResultKt.throwOnFailure($result);
          break;
       default:
          throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
       }
   return "result from request";
}

ResultKt.throwOnFailure($result) 是 Kotlin 协程中的一个重要方法,主要用于处理协程的异常情况。它的主要作用是: (1)检查异常:它会检查传入的 $result 对象,如果这个对象是一个异常(即协程执行过程中抛出的异常),它会立即抛出这个异常。 (2)确保正常执行:如果 $result 不是异常,则继续正常执行后续代码。

挂起过程

函数返回值由 String 变成 Object,编译器自动增加了Continuation参数,相当于帮我们添加Callback。

根据 completion 创建了一个 ContinuationImpl(如果已经创建就直接用,避免重复创建),复写了 invokeSuspend() 方法,在这个方法里面它又调用了 request() 方法,这里又调用了一次自己(是不是很神奇),并且把 continuation 传递进去。

在 switch 语句中,label 的默认初始值为 0,第一次会进入 case 0 分支,delay() 是一个挂起函数,传入上面的 continuation 参数,会有一个 Object 类型的返回值。这个结果要么是COROUTINE_SUSPENDED,否则就是真实结果。

DelayKt.delay(2000, continuation)的返回结果如果是 COROUTINE_SUSPENDED , 则直接 return ,那么方法执行就被结束了,方法就被挂起了。

函数即便被 suspend 修饰了,但是也未必会挂起。需要里面的代码编译后有返回值为 COROUTINE_SUSPENDED 这样的标记位才可以。

协程的挂起实际是方法的挂起,本质是return。

恢复过程

因为 delay() 是 IO操作,在2000ms后就会通过传递给它的 continuation 回调回来。

回调到 ContinuationImpl 类的 resumeWith() 方法,会再次调用 invokeSuspend() 方法,进而再次调用 request() 方法。

即反编译代码中的这一段:

Object continuation;
   if (completion instanceof <undefinedtype>){
     continuation =  <undefinedtype>completion
   }else{
     continuation = new ContinuationImpl(completion) {
       Object result;
       int label; //初始值为0
        
       @Nullable
       public final Object invokeSuspend(@NotNull Object $result) {
          this.result = $result;
          this.label |= Integer.MIN_VALUE;
          return request(this);//又调用了request()方法
       }
    };
  }

程序会再次进入switch语句,由于第一次在 case 0 时把 label = 1 赋值为1,所以这次会进入 case 1 分支,检查无异常之后,再次 break ,并且返回了结果result from request。

并且 request() 的返回值作为 invokeSuspend() 的返回值返回。重新被执行的时候就代表着方法被恢复了。

看到大家一定会疑问, 步骤2中 invokeSuspend() 是如何被再次调用呢? 我们都知道 ContinuationImpl 的父类是 BaseContinuationImpl,实际上ContinuationImpl中调用的resumeWith()是来自父类。

BaseContinuationImpl

internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    //这个实现是最终的,用于展开 resumeWith 递归。
    public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result
        while (true) {
            with(current) {
                val completion = completion!!
                val outcome: Result<Any?> =
                    try {
                        // 1.调用 invokeSuspend()方法执行,执行协程的真正运算逻辑,拿到返回值
                        val outcome = invokeSuspend(param)
                        // 2.如果返回的还是COROUTINE_SUSPENDED则提前结束
                        if (outcome == COROUTINE_SUSPENDED) return 
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                if (completion is BaseContinuationImpl) {
                    //3.如果 completion 是 BaseContinuationImpl,内部还有suspend方法,则会进入循环递归,继续执行和恢复
                    current = completion
                    param = outcome
                } else {
                    //4.否则是最顶层的completion,则会调用resumeWith恢复上一层并且return
                    // 这里实际调用的是其父类 AbstractCoroutine 的 resumeWith 方法
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }
}

实际上任何一个挂起函数它在恢复的时候都会调到 BaseContinuationImpl 的 resumeWith() 方法里面。

一但 invokeSuspend() 方法被执行,那么 request() 又会再次被调用, invokeSuspend() 就会拿到 request() 的返回值,在 ContinuationImpl 里面根据 val outcome = invokeSuspend() 的返回值来判断我们的 request() 方法恢复了之后的操作。

如果 outcome 是 COROUTINE_SUSPENDED 常量(可能挂起函数中又返回了一个挂起函数),说明你即使被恢复了,执行了一下, if (outcome == COROUTINE_SUSPENDED) return但是立马又被挂起了,所以又 return 了。

如果本次恢复 outcome 是一个正常的结果,就会走到 completion.resumeWith(outcome),当前被挂起的方法已经被执行完了,实际调用的是其父类 AbstractCoroutine 的 resumeWith 方法,那么协程就恢复了。

我们知道 request() 肯定是会被协程调用的(从上面反编译代码知道会传递一个Continuation completion参数),request() 方法恢复完了就会让协程completion.resumeWith()去恢复,所以说协程的恢复是方法的恢复,本质其实是 callback(resumeWith) 回调。

一张图总结一下:

协程的核心是挂起——恢复,挂起——恢复的本质是return & callback回调

协程挂起

我们说过协程启动后会调用到上面这个 resumeWith() 方法,接着调用其 invokeSuspend() 方法:

当 invokeSuspend() 返回 COROUTINE_SUSPENDED 后,就直接 return 终止执行了,此时协程被挂起。 当 invokeSuspend() 返回非 COROUTINE_SUSPENDED 后,说明协程体执行完毕了,对于 launch 启动的协程体,传入的 completion 是 AbstractCoroutine 子类对象,最终会调用其 AbstractCoroutine.resumeWith() 方法做一些状态改变之类的收尾逻辑。至此协程便执行完毕了。

协程恢复

这里我们接着看上面第一条:协程执行到挂起函数被挂起后,当这个挂起函数执行完毕后是怎么恢复协程的,以下面挂起函数为例:

private suspend fun login() = withContext(Dispatchers.IO) {
    Thread.sleep(2000)
    return@withContext true
}

通过反编译可以看到上面挂起函数中的函数体也被编译成了 SuspendLambda 的子类,创建其实例时也需要传入 Continuation 续体参数(调用该挂起函数的协程所在续体)。贴下 withContext 的源码:

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        // 创建 new context
        val oldContext = uCont.context
        val newContext = oldContext + context
        // 检查新上下文是否作废
        newContext.ensureActive()
        // 新上下文与旧上下文相同
        if (newContext === oldContext) {
            val coroutine = ScopeCoroutine(newContext, uCont)
            return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
        }
        // 新调度程序与旧调度程序相同
        if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
            val coroutine = UndispatchedCoroutine(newContext, uCont)
            // 上下文有变化,所以这个线程需要更新
            withCoroutineContext(newContext, null) {
                return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
            }
        }
        // 使用新的调度程序
        val coroutine = DispatchedCoroutine(newContext, uCont)
        block.startCoroutineCancellable(coroutine, coroutine)
        coroutine.getResult()
    }
}

首先调用了 suspendCoroutineUninterceptedOrReturn 方法,看注释知道可以通过它来获取到当前的续体对象 uCont, 接着有几条分支调用,但最终都是会通过续体对象来创建挂起函数体对应的 SuspendLambda 对象,并执行其 invokeSuspend() 方法,在其执行完毕后调用 uCont.resume() 来恢复协程,具体逻辑大家感兴趣可以自己跟代码,与前面大同小异。

至于其他的顶层挂起函数如 await(), suspendCoroutine(), suspendCancellableCoroutine() 等,其内部也是通过 suspendCoroutineUninterceptedOrReturn() 来获取到当前的续体对象,以便在挂起函数体执行完毕后,能通过这个续体对象恢复协程执行。

Desktop平台举例分析挂起恢复(Kotlin 2.1.0)

Kotlin测试代码如下:

class MySimpleTest {

    suspend fun stephenTest(): String {
        delay(500L)
        return "result From stephenTest"
    }
}

fun callFromOutside() {
    CoroutineScope(Dispatchers.IO).launch {
        val result = MySimpleTest().stephenTest()
        println(result)
    }
}

在callFromOutside函数中,我们创建了一个协程作用域,并在其中启动了一个协程。该协程将调用stephenTest函数。而stephenTest函数是一个挂起函数,它会暂停该协程的执行,直到delay函数返回。

将这个片段反编译成java代码,删掉导包和元数据注解信息等,分析过程见注释流程号:

public final class MySimpleTest {
   public static final int $stable;

   @Nullable
   // (8)stephenTest函数本来是无参的,现在有一个Continuation类型的参数
   // 这个就是外部调用代码块封装成的实例,stephenTest方法执行完毕,需要继续往下执行的代码都在这个对象里面
   public final Object stephenTest(@NotNull Continuation $completion) {
      Continuation $continuation;
      // (9)label20: 是一个Java中的标签(label),主要用于控制流程跳转。在这里它被用来实现协程的挂起和恢复机制
      label20: {
         if ($completion instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)$completion;
            // (10)用于检查当前协程是否处于挂起状态。Integer.MIN_VALUE 是一个特殊的标志位,用于标记协程是否被挂起。
            // 它的值是10000000 00000000 00000000 00000000
            // label首次传进来是1,即00000000 00000000 00000000 00000001,和Integer.MIN_VALUE按位与的结果为0,表示需要挂起,会走到11步,基于外部传入的 completion 对象创建一个新的ContinuationImpl对象
            //=======================分割线====================
            // (16)这里的label在15步被赋值成了10000000 00000000 00000000 00000001,按位与的结果是Integer.MIN_VALUE,即条件检查结果为真(即 != 0)
            // 10000000 00000000 00000000 00000001减去Integer.MIN_VALUE,结果是1,即00000000 00000000 00000000 00000001
            // 并将label20标签跳出循环,继续往下执行stephenTest的switch状态判断
            if (($continuation.label & Integer.MIN_VALUE) != 0) {
               $continuation.label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         // (11)开始创建关于stephenTest代码块的ContinuationImpl对象,用于传递给下一个suspend函数
         $continuation = new ContinuationImpl($completion) {
            // $FF: synthetic field
            Object result;
            // 初始值为0
            int label;

            @Nullable
            // (13)delay执行完,调用resumeWith,触发这个invokeSuspend方法
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               //(14)将label = 1和Integer.MIN_VALUE按位或,
               // 运算的结果是 10000000 00000000 00000000 00000001(即 -2147483647)
               this.label |= Integer.MIN_VALUE;
               //(15)重入调用stephenTest函数,这次是传入 $continuation 自己作为参数。
               return MySimpleTest.this.stephenTest((Continuation)this);
            }
         };
      }


      Object $result = $continuation.result;
      Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      // (17) 本轮调用中,label值为1,检查无异常后,就会返回这个字符串
      // "result From stephenTest"
      switch ($continuation.label) {
         case 0:
            ResultKt.throwOnFailure($result);
            $continuation.label = 1;
            // (12)调用delay函数,之后就和外部调用的(3)-(7)步流程一样.
            // 传入ContinuationImpl对象,delay函数内部会判断是否需要挂起,如果需要挂起,就return掉本轮stephenTest方法的调用
            // 进入了delay内部执行,等500ms过后,调用外部传进来的ContinuationImpl对象的 resumeWith 函数回调
            // 而resumeWith方法,必然会调用到这个ContinuationImpl 对象自己的invokeSuspend方法,就跳转到第13步了
            if (DelayKt.delay(500L, $continuation) == var4) {
               return var4;
            }
            break;
         case 1:
            ResultKt.throwOnFailure($result);
            break;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      return "result From stephenTest";
   }
}

// CoroutineTestKt.java
public final class CoroutineTestKt {

  
   public static final void callFromOutside() {
      // (1)分析入口,从最外部的调用开始
      BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getIO()), (CoroutineContext)null, (CoroutineStart)null, new Function2((Continuation)null) {

        // (2)函数代码块里的任务,被封装在了继承自Continuation的一个匿名内部类对象中
        // launch开始后,进入就会调用其invoke方法,并首次执行invokeSuspend方法,这时候label为0
         int label;
        // (18) 17步返回后,标志着 stephenTest 方法中 $continuation实例的invokeSuspend方法调用完毕
        // 将调用completion的invokeSuspend方法
         // (19)这时候外部的这个label值也已经为1了,就是继续往下执行了
         public final Object invokeSuspend(Object $result) {
            Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            Object var10000;
            // (3)通过label来判断当前是到了哪一个状态
            switch (this.label) {
               case 0:
                  // (4)首先检查异常
                  ResultKt.throwOnFailure($result);
                  // (5)创建一个MySimpleTest对象,并调用其stephenTest方法
                  var10000 = new MySimpleTest();
                  // (6)将这个匿名内部类自己传进去,作为参数
                  Continuation var10001 = (Continuation)this;
                  // 将label状态设置为1,等下次再次调用invokeSuspend就会走switch的1的分支
                  this.label = 1;
                  var10000 = (MySimpleTest)var10000.stephenTest(var10001);
                   // (7) 如果 stephenTest 这个方法的返回值是COROUTINE_SUSPENDED,则表示该函数已暂停,我们也返回COROUTINE_SUSPENDED给调用者
                  // 通知这个函数是挂起函数,暂时不往下执行了
                  if (var10000 == var3) {
                     return var3;
                  }
                  // 转到MySimpleTest这个类分析 ->(8)
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  var10000 = (MySimpleTest)$result;
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            // (20)挂起和恢复流程执行完毕,打印结果
            String result = (String)var10000;
            System.out.println(result);
            return Unit.INSTANCE;
         }

         public final Continuation create(Object value, Continuation $completion) {
            return (Continuation)(new <anonymous constructor>($completion));
         }

         public final Object invoke(CoroutineScope p1, Continuation p2) {
            return ((<undefinedtype>)this.create(p1, p2)).invokeSuspend(Unit.INSTANCE);
         }

         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object p1, Object p2) {
            return this.invoke((CoroutineScope)p1, (Continuation)p2);
         }
      }, 3, (Object)null);
   }
}

以上的分析流程就是协程的挂起恢复过程。

自动的线程切换

引例

在Android上使用协程,从本地读取一个字符串,或其他耗时逻辑,可能会写下这样的代码:

// MainViewModel.kt
suspend fun getLocalString() = withContext(Dispatchers.IO) {
        // 模拟IO操作
        Thread.sleep(2000) 
        "result from local"
    } 

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 开启协程
        lifecycleScope.launch {
            val result = getLocalString()
            tv_result.text = result
        }
    }
}

getLocalString() 方法,我们设置了上下文为IO,很明显直觉上会在 IO 线程中执行。

MainActivity 中,我们通过 lifecycleScope.launch 开启了一个协程,协程中调用 getLocalString() 方法,最初为主线程环境,是怎么从主线程切换到IO线程来运行这个方法的呢?

ContinuationInterceptor

Kotlin协程实现自动线程切换的核心在于其调度器(Dispatcher)机制,而调度器 Dispatcher 就是 ContinuationInterceptor 的实现。

ContinuationInterceptor 接口(简化):

public interface ContinuationInterceptor : CoroutineContext.Element {
    // 拦截 Continuation 的恢复
    fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
}

AbstractCoroutine (例如 StandaloneCoroutine) 的 resumeWith 方法:

当协程需要恢复时,通常会调用 ContinuationresumeWith 方法。在协程的底层实现中,例如 AbstractCoroutine (所有 Job 的子类,如 StandaloneCoroutine 继承的基类),其 resumeWith 方法会检查 CoroutineContext 中是否存在 ContinuationInterceptor

// AbstractCoroutine.kt (简化)
override fun resumeWith(result: Result<T>) {
    val context = this.context
    val dispatcher = context[ContinuationInterceptor] as? ContinuationInterceptor

    if (dispatcher == null) {
        // 没有调度器,直接在当前线程执行
        dispatchResume(result)
    } else {
        // 有调度器,通过调度器来分发恢复操作
        dispatcher.dispatch(this, result) // 最终会调用 dispatchResume
    }
}

Dispatcherdispatch 方法

Dispatcherdispatch 方法是实际执行线程切换的地方。不同的调度器有不同的实现。

Dispatchers.Default (例如 DefaultScheduler.kt):

Dispatchers.Default 通常使用一个共享的线程池来执行协程。

// DefaultScheduler.kt (简化)
internal object DefaultScheduler : CoroutineDispatcher(), Executor {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        // 将 block (即恢复协程的Runnable) 提交到默认的线程池
        DefaultExecutor.enqueue(block) // DefaultExecutor 是一个线程池
    }
    // ... 其他方法
}

dispatch 被调用时,它会将协程的恢复逻辑封装在一个 Runnable 中,然后提交给调度器底层的 线程池 。这个 Runnable 最终会在线程池中的某个线程上执行,从而实现了线程的切换。

  • Dispatchers.IO 通常会使用一个独立的、容量更大的线程池,用于处理 IO 密集型任务。其 dispatch 逻辑与 Default 类似,只是提交给不同的线程池。
  • Dispatchers.Main 在 Android 上通常会与主线程的 Looper 绑定。
// AndroidMainDispatcherFactory.kt (简化)
internal class AndroidMainDispatcherFactory : MainDispatcherFactory {
    override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
        // ...
        return HandlerContext(Looper.getMainLooper(), "Main") // 包装了 Looper
    }
}

// HandlerDispatcher.kt (简化)
class HandlerContext(...) : MainCoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        // 将 block 提交到 Looper 关联的 Handler
        handler.post(block)
    }
    // ...
}

上面的代码可以看出, Dispatchers.Main 会将协程的恢复操作通过 Android Handlerpost 方法发送到主线程的消息队列,从而确保协程在主线程上恢复执行。

suspendCoroutine / suspendCancellableCoroutine

除了编译器自动生成的挂起点,我们也可以手动创建挂起点,这通常通过 suspendCoroutinesuspendCancellableCoroutine 函数实现。

suspend fun manualSuspendExample(): String = suspendCancellableCoroutine { continuation ->
    // 可以在这里执行一些异步操作
    Thread {
        Thread.sleep(1000)
        continuation.resume("Resumed from another thread") // 在另一个线程调用 resume
    }.start()
}

这里 continuation.resume(...) 的调用是关键。当这个 resume 被调用时,它会触发之前提到的 ContinuationInterceptor 机制,如果 CoroutineContext 中有调度器,就会通过调度器进行线程切换。

总结挂起和线程切换流程

  1. 协程挂起: 当协程遇到 suspend 函数(如 delay,或自定义的挂起函数),如果该函数需要等待某个异步操作完成,它会返回 COROUTINE_SUSPENDED,并将当前的执行上下文(Continuation)保存起来。
  2. 异步操作完成: 当异步操作完成时(例如网络请求返回数据,或 delay 时间到),会调用之前保存的 Continuation 对象的 resumeWith 方法。
  3. 调度器介入: resumeWith 方法会检查协程的 CoroutineContext 中是否存在 ContinuationInterceptor (即 Dispatcher)。
  4. 分发恢复: 如果存在调度器,resumeWith 会调用调度器的 dispatch 方法。
  5. 线程切换: 调度器的 dispatch 方法会将协程的恢复逻辑(一个 Runnable)提交到其管理的线程(如线程池中的线程,或 Android 主线程)。
  6. 协程恢复: Runnable 在目标线程上执行,调用实际的恢复逻辑,协程从挂起的地方继续执行。

通过这种 CPS转换 + 回调resume + 调度器拦截 的机制,Kotlin 协程得以在不阻塞线程的情况下,根据需要自动在不同的线程之间切换执行,从而实现高效的并发编程。

【Kotlin】Kotlin中的集合

【Kotlin】Kotlin中的集合

本文介绍了Kotlin中集合的相关,并列举一下其使用方式

在 Kotlin 中,集合类(Collections)是非常重要的一部分,它提供了丰富且功能强大的 API 来操作数据集合。Kotlin 的集合类在很大程度上与 Java 的集合框架兼容,但 Kotlin 在其基础上进行了扩展和增强,提供了更简洁、更安全、更富表达力的 API。

一个重要的概念是,Kotlin 明确区分了只读 (read-only) 集合和可变 (mutable) 集合。

  • 只读集合接口: 它们只提供读取数据的方法,不能添加、删除或修改元素。例如:List<T>、Set<T>、Map<K, V>
  • 可变集合接口: 它们在只读接口的基础上,提供了修改集合的方法。例如:MutableList<T>、MutableSet<T>、MutableMap<K, V>

Kotlin 的集合类主要分为三大类:列表 (List)、集合 (Set) 和 映射 (Map)。这些集合类都继承自 kotlin.collections 包中的接口。

List<T>

有序集合,保持元素的插入顺序,其内部的元素可以重复。

  • 只读列表:通常通过 listOf() 或 mutableListOf().toList() 创建。
  • 可变列表:通常通过 mutableListOf() 创建。除了 List 的功能外,还支持添加、删除、更新元素。

除了上面两种创建方式,还可以使用 arrayListOf() 返回一个 ArrayList ,其实就是Java 的 ArrayList。

Set<T>

Set是一种无序的集合,不包含重复的元素。

  • 只读集合 通常通过 setOf()mutableSetOf().toSet() 创建。不保证元素的顺序,不允许有重复元素。
  • 可变集合 通常通过 mutableSetOf() 创建。支持添加、删除元素。

其他的创建方式还有 hashSetOf(),返回一个 HashSet (Java 的 HashSet)。

linkedSetOf(): 返回一个 LinkedHashSet (Java 的 LinkedHashSet)。

Map<K, V>

映射 (也称为字典或关联数组) 存储键值对,其中每个键都是唯一的,并且映射到一个值。

  • Map<K, V> (只读): 通常通过 mapOf()mutableMapOf().toMap() 创建。用来存储键值对,键是唯一的,不保证元素的顺序 (除非使用特定实现如 LinkedHashMap)。
  • MutableMap<K, V> (可变): 通常通过 mutableMapOf() 创建。支持添加、删除、更新键值对。

还可以使用 hashMapOf() 创建一个 HashMap (Java 的 HashMap)。

linkedMapOf(): 返回一个 LinkedHashMap (Java 的 LinkedHashMap)。

Kotlin 集合与 Java 集合对比

Kotlin 的集合在很大程度上是基于 Java 集合框架的,但进行了优化和扩展,提供了更安全、更简洁的 API。

1. 只读与可变分离 (最主要区别)

Kotlin: 明确区分了只读接口 (List, Set, Map) 和可变接口 (MutableList, MutableSet, MutableMap)。

这在编译时强制执行了不变性,有助于避免运行时错误和并发问题。当你只需要读取集合时,声明为只读类型可以更好地表达意图,并防止意外修改。

val readOnlyList: List<String> = listOf("A", "B", "C")
// readOnlyList.add("D") // 编译错误!

val mutableList: MutableList<String> = mutableListOf("X", "Y", "Z")
mutableList.add("W") // 可以修改

而使用 Java的集合接口 (如 List, Set, Map) 本身就包含了修改方法。虽然可以通过 Collections.unmodifiableList() 等方法创建不可修改的视图,但这只是一个运行时检查,如果你仍然持有原始的可变集合引用,它仍然可以被修改。

List<String> javaList = new ArrayList<>(Arrays.asList("A", "B", "C"));
// javaList.add("D"); // 可以直接修改

List<String> unmodifiableJavaList = Collections.unmodifiableList(javaList);
// unmodifiableJavaList.add("E"); // 运行时抛出 UnsupportedOperationException
// 但是,如果修改 javaList,unmodifiableJavaList 也会随之改变
javaList.add("F"); // unmodifiableJavaList 现在也包含 "F"

在Java中也可以直接创建不可变集合,使用 Java 9 以后引入的 List.of(), Set.of(), Map.of() 方法。

List<Integer> numbers = Arrays.asList(1, 2, 3); // 返回一个固定大小的List
// 或者
List<Integer> numbersJava9 = List.of(1, 2, 3); // 不可变List
Map<String, Integer> users = new HashMap<>();
users.put("Alice", 30);

2. 可空性支持

Kotlin 的类型系统原生支持可空性。这意味着你可以明确指定集合是否可以包含 null 元素,以及集合本身是否可以为 null

val nullableStrings: List<String?> = listOf("A", null, "B") // 列表中可以有 null
val nonNullableList: List<String> = listOf("C", "D") // 列表中不能有 null

// 如果一个List本身可能为null
var maybeList: List<Int>? = null
maybeList = listOf(1, 2)

而 Java 在语言层面没有原生支持可空性,null 是一种常见的运行时错误源 (NullPointerException)。通常通过 @Nullable@NonNull 注解来提示,但这些只是编译器或工具的提示,不能像 Kotlin 那样在编译时强制执行。

3. 集合扩展函数

Kotlin提供了大量的 扩展函数 (Extension Functions) 来操作集合,这使得集合操作变得非常简洁和富有表现力。例如:filter, map, forEach, firstOrNull, count, groupBy 等。这些函数通常链式调用,形成了非常强大的函数式编程风格。

val nums = listOf(1, 2, 3, 4, 5)
val evenSquared = nums.filter { it % 2 == 0 }.map { it * it } // 过滤偶数并平方
println(evenSquared) // 输出: [4, 16]

以下是一些常用的扩展函数:

  • map:对集合中的每个元素进行转换,返回新的集合。
  • filter:过滤出符合条件的元素,返回新的集合。
  • flatMap:先对每个元素进行转换,然后将结果扁平化为一个新的集合。
  • reducefold:对集合中的元素进行累积操作。
  • forEach:遍历集合中的每个元素。
  • anyall:判断集合中是否存在或所有元素满足某个条件。
  • findfirst:查找符合条件的元素。

示例:

val numbers = listOf(1, 2, 3, 4, 5)

val doubled = numbers.map { it * 2 } // [2, 4, 6, 8, 10]
val even = numbers.filter { it % 2 == 0 } // [2, 4]
val sum = numbers.reduce { acc, i -> acc + i } // 15

这些扩展函数让 Kotlin 的集合操作更加直观和简洁,提高了开发效率。

4. 与 Java 集合的互操作性

Kotlin 集合与 Java 集合是完全兼容的,并且可以无缝互操作。

  • 在 Kotlin 代码中,你可以直接使用 Java 的 ArrayList, HashSet, HashMap 等。当你在 Kotlin 中使用这些 Java 集合时,它们会自动被视为可变集合。
  • 当 Kotlin 的只读集合传递给 Java 方法时,它们会被转换为相应的 Java 接口,但仍然是“只读视图”。修改这些视图会导致运行时异常,而修改原始 Kotlin 可变集合则会反映在 Java 视图中。
  • 当你从 Java 方法接收集合时,Kotlin 会将其视为可变集合,但在 Kotlin 中你可以轻松地将其转换为只读视图(例如 someJavaList.toList())。

Kotlin 的 Sequence(序列)

Sequence 是 Kotlin 提供的一种惰性集合操作机制,类似于 Java 的 Stream API。它的主要特点是:

  • 惰性计算Sequence 中的操作不会立即执行,而是按需计算,只有在终端操作(如 toList()forEach())被调用时才会执行。
  • 适合大数据集:由于是惰性计算,Sequence 在处理大量数据时更高效,因为它避免了创建中间集合。
  • 链式操作:支持链式调用多个操作,代码更简洁。

示例:

val numbers = sequenceOf(1, 2, 3, 4, 5)

val result = numbers
    .map { it * 2 }       // 不会立即执行
    .filter { it % 3 == 0 } // 不会立即执行
    .toList()             // 触发实际计算,返回 [6]

println(result) // 输出 [6]

与 Java 的 Stream 相比,Kotlin 的 Sequence 在语法上更简洁,且与 Kotlin 的集合体系无缝集成。

【Kotlin】Kotlin协变和逆变

【Kotlin】Kotlin协变和逆变

本文介绍了Kotlin中泛型相关的协变和逆变,和reified关键字

协变(Covariance)、逆变(Contravariance)和 reified 关键字是 Kotlin 泛型系统中比较高级和强大的特性。它们能帮助你编写更健壮、更灵活、更类型安全的泛型代码,尤其是在处理集合、高阶函数以及需要运行时类型检查的场景。

类型擦除

在深入协变和逆变之前,先简单回顾一下 Java/Kotlin 泛型的类型擦除(Type Erasure)

在 JVM 上,泛型信息只在编译时存在,运行时会被擦除。这意味着 List<String>List<Int> 在运行时都会变成 List<Object>(或 List<Any?>)。

这就导致了两个主要限制:

  1. 你不能在运行时直接获取泛型参数的具体类型(比如 T::class.java)。
  2. 你不能直接创建泛型数组(比如 Array<T>())。

Kotlin 中通过 reified 关键字解决了第一个限制,在内联函数中使用,可以在编译期就确定泛型参数的实际类型。

协变和逆变 则解决了在使用泛型时如何安全地处理子类型关系的问题。

Java中的协变和逆变

首先回顾下Java中是怎么做的, Java 泛型中的 superextends 通配符,与 Kotlin 的协变 (out) 和逆变 (in) 概念密切相关。

Java 泛型通配符:extendssuper

Java 中,泛型默认是 不变的 (invariant) ,这意味着 List<String> 并不是 List<Object> 的子类型,也就是说,子类的泛型(List<String>)不属于泛型(List<Object>)的子类,反之亦然。

为了在需要时放宽这种限制,Java 引入了泛型通配符? extends T? super T

它们允许你在泛型类型参数上定义上限或下限,从而实现 协变(Covariance)逆变(Contravariance) 的效果。

1. ? extends T (上界通配符)

简介:

  • 含义: ? extends T 表示“类型是 T 或 T 的某个子类型”。
  • 用途: 主要用于从泛型结构中读取数据。你可以从一个 List<? extends T> 中获取 T 类型的对象,但不能安全地向其中添加任何 T 类型的对象(除了 null)。
  • 角色: 充当生产者 (Producer)。如果你要从集合中东西,那么这个集合应该使用 extends
  • 与 Kotlin 的 out 对应: ? extends T 在 Java 中实现了协变

如果 Sub 是 Super 的子类型,那么 Generic<Sub> 也是 Generic<Super> 的子类型,就称为协变。即 子类型关系在泛型中得以保留。

List<Button> buttons = new ArrayList<Button>();
List<? extends TextView> textViews = buttons; // 合法
TextView textView = textViews.get(0); // 合法

// 下面的描述都是成立的
List<? extends TextView> textViews = new ArrayList<TextView>(); // 👈 本身
List<? extends TextView> textViews = new ArrayList<Button>(); // 👈 直接子类
List<? extends TextView> textViews = new ArrayList<RadioButton>(); // 👈 间接子类

前面说到 List<? extends TextView> 的泛型类型是个未知类型 ?,编译器也不确定它是啥类型,只是有个限制条件。

由于它满足 ? extends TextView 的限制条件,所以 get 出来的对象,肯定是 TextView 的子类型,根据多态的特性,能够赋值给 TextView,啰嗦一句,赋值给 View 也是没问题的。

List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0); // 合法
View view = textViews.get(0); // 合法

// 下面的添加元素的代码是不合法的
textViews.add(new Button()); // 不合法
textViews.add(new TextView()); // 不合法

到了 add 操作的时候,我们可以这么理解:

  • List<? extends TextView> 由于类型未知,它可能是 List<Button>,也可能是 List<TextView>
  • 对于前者,显然我们要添加 TextView 是不可以的。
  • 实际情况是编译器无法确定到底属于哪一种,无法继续执行下去,就报错了。

2. ? super T (下界通配符)

简介:

  • 含义: ? super T 表示“类型是 T 或 T 的某个父类型”。
  • 用途: 主要用于向泛型结构中写入数据。你可以向一个 List<? super T> 中添加 T 类型的对象或其任何子类型,但从其中获取元素时,你只能确定它们是 Object 类型。
  • 角色: 充当消费者 (Consumer)。如果你要向集合中东西,那么这个集合应该使用 super
  • 与 Kotlin 的 in 对应: ? super T 在 Java 中实现了逆变

如果 Sub 是 Super 的子类型,那么 Generic<Super>Generic<Sub> 的子类型,就称为逆变。即 子类型关系在泛型中被反转。

先看一下它的写法:

List<? super Button> buttons = new ArrayList<TextView>();

这个 ? super 叫做「下界通配符」,可以使 Java 泛型具有「逆变性 Contravariance」。

与上界通配符对应,这里 super 限制了通配符 ? 的子类型,所以称之为下界。

它也有两层意思:

  • 通配符 ? 表示 List 的泛型类型是一个未知类型。
  • super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件。
    • super 我们在类的方法里面经常用到,这里的范围不仅包括 Button 的直接和间接父类,也包括下界 Button 本身。
    • super 同样支持 interface

上面的例子中, TextViewButton 的父类型 ,也就能够满足 super 的限制条件,就可以成功赋值了。

其他示例:

List<? super Button> buttons = new ArrayList<Button>(); // 👈 本身
List<? super Button> buttons = new ArrayList<TextView>(); // 👈 直接父类
List<? super Button> buttons = new ArrayList<Object>(); // 👈 间接父类

在涉及到拿取和添加元素的情景时,编译器可以确定你 添加进去的元素是 Button 的父类 ,Button 对象一定是这个未知类型的子类型,根据多态的特性,这里通过 add 添加 Button 对象是合法的。

但你不能通过 get 方法拿到这个元素,因为编译器只知道它是个未知类型,是 Button 的父类,但是你拿什么类型的对象来接收呢(除非Object)。

使用下界通配符 ? super 的泛型 List,只能读取到 Object 对象,一般没有什么实际的使用场景,通常也只拿它来添加数据,也就是消费已有的 List<? super Button>,往里面添加 Button,因此这种泛型类型声明称之为「消费者 Consumer」。

Kotlin中的 协变:out 关键字

以上为java中实现逆变和协变的方法,在Kotlin中的写法如何呢?在 Kotlin 中,当泛型类型参数被标记为 out 时,它表示该类型参数只能被生产(作为返回值输出),而不能被消费(作为参数输入)。(这个很形象,一个out,一个in)

  • 如果一个类 Producer<T> 的类型参数 T 被声明为 out
    • Producer<T> 的成员函数只能返回 T 类型的值
    • Producer<T> 的成员函数不能接受 T 类型的值作为参数(因为你无法保证传入的 T 是特定子类型)。
  • 这意味着,如果 AB 的子类型,那么 Producer<A> 就是 Producer<B> 的子类型。
// 声明一个协变接口:只能生产 T 类型
interface Producer<out T> {
    fun produce(): T // T 只能作为返回类型(生产)
    // fun consume(item: T) // 编译错误!T 不能作为参数类型(消费)
}

open class Animal
class Cat : Animal()
class Dog : Animal()

// 实现生产 Animal 的生产者
class AnimalProducer : Producer<Animal> {
    override fun produce(): Animal = Cat() // 可以生产 Cat (是 Animal 的子类)
}

// 实现生产 Cat 的生产者
class CatProducer : Producer<Cat> {
    override fun produce(): Cat = Cat()
}

fun main() {
    val animalProducer: Producer<Animal> = CatProducer() // 协变:CatProducer 可以被赋值给 Producer<Animal>
    val animal: Animal = animalProducer.produce() // produce() 返回 Animal
    println("Produced: $animal")
    // AnimalProducer producerCat = CatProducer() // 这样也是可以的
}

何时使用 out 当你的泛型类型只作为输出(例如,函数返回值、只读属性)时,使用 out。这通常用于表示“提供者”或“源头”。Kotlin 的 List<out E> 就是一个很好的例子:你只能从 List 中获取元素,不能添加特定类型的元素(尽管 MutableList<E> 不会使用 out,因为它可以添加)。

Kotlin中的逆变:in 关键字

在 Kotlin 中,当泛型类型参数被标记为 in 时,它表示该类型参数只能被消费(作为参数输入),而不能被生产(作为返回值输出)。

  • 如果一个类 Consumer<T> 的类型参数 T 被声明为 in
    • Consumer<T> 的成员函数只能接受 T 类型的值作为参数
    • Consumer<T> 的成员函数不能返回 T 类型的值(因为你无法保证返回的 T 是特定父类型)。
  • 这意味着,如果 AB 的子类型,那么 Consumer<B> 就是 Consumer<A> 的子类型。
// 声明一个逆变接口:只能消费 T 类型
interface Consumer<in T> {
    fun consume(item: T) // T 只能作为参数类型(消费)
    // fun produce(): T // 编译错误!T 不能作为返回类型(生产)
}

open class Animal
class Cat : Animal()
class Dog : Animal()

// 实现消费 Animal 的消费者
class AnimalConsumer : Consumer<Animal> {
    override fun consume(item: Animal) {
        println("Consuming an animal: $item")
    }
}

// 实现消费 Cat 的消费者
class CatConsumer : Consumer<Cat> {
    override fun consume(item: Cat) {
        println("Consuming a cat: $item")
    }
}

fun main() {
    val catConsumer: Consumer<Cat> = AnimalConsumer() // 逆变:AnimalConsumer 可以被赋值给 Consumer<Cat>
    catConsumer.consume(Cat()) // 可以消费 Cat
    // catConsumer.consume(Animal()) // 编译错误!因为 catConsumer 期望的是 Cat 或其子类型
}

何时使用 in 当你的泛型类型只作为输入(例如,函数参数、只写属性)时,使用 in。这通常用于表示“消费者”或“汇集点”。Kotlin 的 Comparator<in T> 就是一个很好的例子:它可以通过比较任何 T 或其超类型来比较 T

reified 关键字

最后介绍一下Kotlin中的reifeid关键字,reified 关键字用于 内联函数 (inline functions) 的泛型类型参数。它解决了 Java/Kotlin 泛型类型擦除的问题,允许你 在运行时访问泛型类型信息

由于类型擦除,你不能像下面的示例一样写,直接在运行时检查一个泛型类型:

// 这是不允许的,因为 T 在运行时是 Any/Object
fun <T> checkIfString(value: Any) {
    // if (value is T) { // 编译错误!Cannot check for instance of erased type: T
    //    println("It's a T")
    // }
}

// 也不允许获取 T 的 Class 对象
// fun <T> createInstance(): T {
//    return T::class.java.newInstance() // 编译错误!Cannot use T as reified type parameter
// }

reified 的作用

当一个泛型类型参数被标记为 reified 时,Kotlin 编译器会在编译时将该类型参数的具体类型信息内联到调用点。这意味着在运行时,该泛型类型不再被擦除,你可以像访问普通类型一样访问它。

  • reified 只能用于 inline 函数的类型参数。因为内联函数会将其代码复制到调用点,所以编译器有机会“知道”实际的类型参数。
  • 有了 reified,你就可以在函数体内使用 is 运算符、as 运算符以及 T::class.java
// 使用 reified 关键字检查类型
inline fun <reified T> T.checkClassType() {
    // 类型 T 内联解析
    when (this) {
        is Int -> {
            // 检查 this 是否为 Int 类型
            println("this is a Int: $this")
        }

        is String -> {
            // 检查 this 是否为 String 类型
            println("this is a String: $this")
        }

        else -> {
            // 检查 this 是否为其他类型
            println("this is a other type: $this")
        }
    }
}

/**
this is a Int: 2
this is a String: Kotlin
this is a other type: 2.0
*/

// 使用reified创建类实例
class Fish {
    fun swim() {
        println("Fish is swimming")
    }
}

inline fun <reified T> createInstance(){
    try {
        // 1. 获取 ClassLoader
        val classLoader = Thread.currentThread().contextClassLoader
        // 2. 加载类
        val className = T::class.java.name
        val loadedClass = classLoader?.loadClass(className)
        // 3. 创建实例
        val instance = loadedClass?.getDeclaredConstructor()?.newInstance()

        instance?.let {
            // 4. 调用方法
            val method = loadedClass.getDeclaredMethod("swim")
            method.invoke(instance)
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

/**
Fish is swimming
*/

reified 在Android中的应用场景

  1. JSON 解析库: 许多 JSON 解析库(如 Gson, Moshi, kotlinx.serialization)的扩展函数使用 reified 来简化类型指定,无需传递 Class<T> 参数。
    // 假设你有这样一个扩展函数
    inline fun <reified T> String.fromJson(): T {
        // 内部使用 T::class.java 进行类型反序列化
        // ...
        throw NotImplementedError()
    }
    
    // val user = jsonString.fromJson<User>() // 比 jsonString.fromJson(User::class.java) 更简洁
    
  2. 启动 Activity: 简化 Activity 的启动,无需在 Intent 中指定 Class
    inline fun <reified T : Activity> Context.startActivity() {
        startActivity(Intent(this, T::class.java))
    }
    
    // 使用:context.startActivity<DetailActivity>()
    
  3. 获取 Service: 简化获取系统服务。安卓热门网络请求库Retrofit也是使用了这个方法来示例化定义好的api服务的。
    inline fun <reified T> Context.getSystemService(): T? {
        return getSystemService(T::class.java) as? T
    }
    
    // val locationManager = context.getSystemService<LocationManager>()
    
  4. 查找视图: 在一些旧的视图查找框架中,可以使用 reified 简化类型转换。

注意事项:

  • reified 只能用于 inline 函数。
  • 由于内联的特性,过度使用 reified 可能会导致生成的字节码文件变大。应合理使用。

【Kotlin】Kotlin专项总结

【Kotlin】Kotlin专项总结

本文介绍了Kotlin语言对比java的优点,以及使用中的一些重难点

本文基于公司内部我写的一篇关于Kotlin的推广文,呼吁在日常开发中更多地使用Kotlin,而不是Java。

Kotlin在Android平台上,最吸引人的一点,就是它在简洁优雅的同时,完全兼容Java,可以与Java的方法,类等无缝地进行互调用。第一章节,先介绍一下对比Java的写法优化。

第二节是Kotlin的一些高级特性,像协程,密封类,内联,noinline等。

对比Java有哪些写法优化

lambda

Java 中也有lambda,在Kotlin中的lambda表达式,是一种更简洁的函数表示方式,它可以代替匿名内部类的使用。lambda表达式的语法如下:

// 无参lambda表达式
val printName = { println("Kotlin") }

// 带参lambda表达式
val sum = { a: Int, b: Int -> a + b }

在这个例子中,printName 是一个无参的lambda表达式,它的函数体只有一行代码。sum 是一个带参的lambda表达式,它接收两个 Int 类型的参数,并返回它们的和。

与匿名内部类相比,lambda表达式的代码更简洁,可读性更好。

其他的用法,例如声明Runnable和线程的时候,可以直接使用lambda写成下面这样:

val runnable = Runnable {
    println("Kotlin")
}

val thread = Thread(runnable)
thread.start()
fun startThread() {
  Thread {
    println("Thread name is ${Thread.currentThread().name}")
  }.start()
}

实现原理

Kotlin和Java的Lambda语法实现均是基于函数式接口(内部只有一个方法的接口)。

函数式接口​​ 是指​​只包含一个抽象方法​​(Single Abstract Method,简称 SAM)​,但是可以有多个默认方法或静态方法的接口。这样的接口可以被 Lambda 表达式 或 方法引用 所实现(或替代)。

Java中没有原生的接口类,而Kotlin则原生定义了很多接口类,归类叫做FunctionN,其中N代表参数的数量,最多支持带22个参数。

public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}

...

同时Kotlin还支持带接收者的lambda,可以说是和函数式编程和扩展函数的结合,可以在lambda的域中访问该对象的变量和方法。

在定义使用Lambda时,会默认将lambda参数继承实现 FunctionN 接口,传递到方法中,在方法中调用 invoke方法。

// HighOrderFunction.kt 文件的顶层函数
fun printSomething(print: () -> Unit) {
    print()
}

反编译之后:

public final class HighOrderFunctionKt {
   public static final void printSomething(@NotNull Function0 print) {
      Intrinsics.checkNotNullParameter(print, "print");
      print.invoke();
   }
}

循环中使用lambda的坑

上面的分析可以得知,每一个lambda的调用,不像一般的方法使用指针来调用,而是都会创建出一个匿名内部类,如果在循环中使用的话,会导致性能问题。

这时候一般会在循环中使用inline关键字修饰的内联函数,或者使用crossinline关键字修饰的内联函数,这样在循环中使用lambda时,就不会创建出多个匿名内部类了。

class LambdaTest {
    inline fun testInline(lambdaParams:()->Unit) {
        lambdaParams()
    }
}

fun main() {
    val lambdaTest = LambdaTest()
    for (i in 0..100000) {
        lambdaTest.testInline {
            println("hello world")
        }
    }
}

反编译之后:

public final class LambdaTest {
   public final void testInline(@NotNull Function0 lambdaParams) {
      Intrinsics.checkNotNullParameter(lambdaParams, "lambdaParams");
      lambdaParams.invoke();
   }
}

public final class MainKt {
   public static final void main() {
      LambdaTest lambdaTest = new LambdaTest();
      int $i$iv = 0;
      int var3;
      for(var3 = 100000; $i$iv <= var3; ++$i$iv) {
         System.out.println("hello world");
      }
   }
}

默认函数参数

Kotlin中函数的参数可以有默认值,这样在调用函数时如果没有为该参数传入值,就会使用默认值。

fun printName(name: String = "Unknown") {
    println("My name is $name")
}

printName() // 输出: My name is Unknown
printName("Kotlin") // 输出: My name is Kotlin

自动类型推断

Kotlin 编译器会根据上下文推断变量的类型,这意味着你通常不需要显式地声明变量的类型。

val name = "Kotlin" // 编译器推断 name 为 String 类型

除了变量,函数也可以自动推断参数类型和返回值类型。

fun sum(a: Int, b: Int): Int {
    return a + b
}

在这个例子中,sum 函数的参数 ab 类型都是 Int,返回值类型也是 Int。Kotlin 编译器可以根据函数体推断出这一点,所以你可以省略函数声明中的类型。

fun sum(a: Int, b: Int) = a + b

if else直接返回结果

Java中,对一个变量进行分支判断赋值,往往写成下面这样:

String name;
if (isMale) {
  name = "Mike";
} else {
  name = "Marry";
}

而Kotlin中,使用if else表达式,在一行代码里完成对变量的赋值:

val name = if (isMale) "Mike" else "Marry"

实际上反编译成Java之后,可以看出这段代码仍然使用的是上面Java的那种写法,或者一个三元判断运算符来实现,不过在面向程序员时的写法更优雅了。

when 关键字

Kotlin 中的 when 关键字,它是一个非常强大和灵活的控制流结构,是 Java 中 switch 语句的增强版。 when 在处理多种条件分支时,比传统的 if-else if-else 链更加简洁和表达性强。

when 可以作为一个表达式(有返回值)或一个语句(没有返回值)使用,这使得它比 Java 的 switch 更具通用性。它的基本作用是根据某个值或条件,执行对应的代码块。

1. when 作为表达式

when 作为表达式使用时,它会评估每个分支的条件,然后返回第一个满足条件的分支的结果。所有可能的分支都必须被覆盖(或者有一个 else 分支),以确保 when 总是能返回一个值。

fun getColorName(colorCode: Int): String {
    return when (colorCode) {
        0xFF0000 -> "Red"
        0x00FF00 -> "Green"
        0x0000FF -> "Blue"
        else -> "Unknown Color" // else 分支是必需的,因为 when 是表达式
    }
}

val color1 = getColorName(0xFF0000) // color1 = "Red"
val color2 = getColorName(0x00FFFF) // color2 = "Unknown Color"
println(color1)
println(color2)

2. when 作为语句

when 作为语句使用时,它会执行第一个满足条件的分支的代码,但不会返回任何值。在这种情况下,else 分支是可选的,除非编译器无法确定所有可能的情况都已覆盖(例如处理 sealed 类时)。

fun printColorInfo(colorCode: Int) {
    when (colorCode) {
        0xFF0000 -> println("This is the color Red.")
        0x00FF00 -> println("This is the color Green.")
        0x0000FF -> println("This is the color Blue.")
        // else 分支在这里是可选的
    }
}

printColorInfo(0x00FF00) // 输出: This is the color Green.

匹配多个值 (逗号分隔)

如果多个分支需要执行相同的操作,可以将它们用逗号 , 分隔开。

val character = 'a'
when (character) {
    'a', 'e', 'i', 'o', 'u' -> println("It's a vowel.")
    in 'b'..'z' -> println("It's a consonant.") // 后面会介绍范围匹配
    else -> println("Not a letter.")
}

范围 (Ranges) 匹配 (in!in)

可以使用 in 运算符检查值是否在一个范围内,或使用 !in 检查是否不在一个范围内。

val age = 25
val category = when (age) {
    in 0..12 -> "Child"
    in 13..19 -> "Teenager"
    in 20..64 -> "Adult"
    else -> "Senior"
}
println("Age $age is a $category.") // 输出: Age 25 is a Adult.

类型检查 (is!is)

可以使用 is 运算符检查一个值是否是某种类型,或使用 !is 检查是否不是某种类型。这在处理多态性或检查未知对象类型时非常有用。

fun describe(obj: Any) {
    when (obj) {
        1 -> println("One")
        "Hello" -> println("Greeting")
        is Long -> println("Long type value: $obj") // obj 会被智能转换为 Long
        !is String -> println("Not a String")
        else -> println("Unknown type or value")
    }
}

describe(1)      // 输出: One
describe("Hello") // 输出: Greeting
describe(1000L)  // 输出: Long type value: 1000
describe(2.5)    // 输出: Not a String
describe("Kotlin") // 输出: Unknown type or value

when 无参数

when 也可以在没有参数的情况下使用。在这种情况下,它会评估每个分支的布尔表达式,然后执行第一个为 true 的分支。这类似于一个更可读的 if-else if-else 链。

val temperature = 28
val isRaining = true

when {
    temperature > 30 -> println("It's very hot!")
    temperature > 20 && !isRaining -> println("It's warm and sunny.")
    isRaining -> println("It's raining.")
    else -> println("Normal weather.")
}
// 输出: It's warm and sunny.

处理密封类 (Sealed Classes) 或枚举 (Enums)

when 在处理密封类 (Sealed Classes)枚举 (Enums) 时特别有用。如果 when 表达式覆盖了密封类或枚举的所有可能子类/值,那么不需要 else 分支,因为编译器可以验证所有情况都已处理。

// 定义一个密封类
sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val message: String) : Result()
    object Loading : Result() // 单例对象
}

fun handleResult(result: Result) {
    when (result) {
        is Result.Success -> println("Success: ${result.data}")
        is Result.Error -> println("Error: ${result.message}")
        Result.Loading -> println("Loading data...") // 注意这里直接引用单例对象
    }
}

handleResult(Result.Success("Data fetched!")) // 输出: Success: Data fetched!
handleResult(Result.Error("Network failed.")) // 输出: Error: Network failed.
handleResult(Result.Loading)                 // 输出: Loading data...

这种用法在 Android 中处理网络请求状态、UI 事件或不同的视图状态时非常常见和强大,因为它提供了编译时安全,确保你不会遗漏任何一种情况。

范围限制coerceIn

coerceIn 是一个扩展函数,作用是将接收者对象的值“强制”限定在一个指定的范围内。如果原始值在这个范围内,就返回原始值;如果原始值小于范围的最小值,就返回最小值;如果原始值大于范围的最大值,就返回最大值。

这个函数通常用于任何实现了 Comparable 接口的类型,比如数字(Int, Double, Float, Long 等)、字符串,甚至自定义的可比较对象。

fun main() {
    // 1. 限定整数范围
    val num1 = 5.coerceIn(1, 10)  // 5 在 [1, 10] 之间,返回 5
    val num2 = 0.coerceIn(1, 10)  // 0 小于 1,返回 1
    val num3 = 12.coerceIn(1, 10) // 12 大于 10,返回 10
    println("Int coercing: $num1, $num2, $num3") // 输出: Int coercing: 5, 1, 10

    // 2. 限定浮点数范围
    val float1 = 3.5f.coerceIn(1.0f, 5.0f) // 3.5f 在 [1.0f, 5.0f] 之间,返回 3.5f
    val float2 = 0.5f.coerceIn(1.0f, 5.0f) // 0.5f 小于 1.0f,返回 1.0f
    println("Float coercing: $float1, $float2") // 输出: Float coercing: 3.5, 1.0

    // 3. 限定字符串范围 (按字典顺序)
    val str1 = "banana".coerceIn("apple", "orange") // banana 在 apple 和 orange 之间,返回 banana
    val str2 = "cat".coerceIn("apple", "banana")    // cat 大于 banana,返回 banana
    val str3 = "zoo".coerceIn("apple", "orange")    // zoo 大于 orange,返回 orange
    println("String coercing: $str1, $str2, $str3") // 输出: String coercing: banana, banana, orange

    // 4. 处理负数或范围倒置(注意:如果 min > max,会抛出 IllegalArgumentException)
    // val invalidRange = 5.coerceIn(10, 1) // 这会抛出 IllegalArgumentException
}

Android 应用 coerceIn

在 Android 开发中,coerceIn 在很多场景下都能派上用场:

  1. UI 元素的滑动或拖拽限制: 当用户拖拽一个视图时,你可能需要限制其位置在屏幕的某个特定区域内。
    val newX = event.rawX.coerceIn(0f, screenWidth - viewWidth)
    view.x = newX
    
  2. 进度条或评分: 确保进度值或评分值始终在有效的 0 到 100(或 1 到 5)范围内。
    val progress = (rawProgressValue * 100).toInt().coerceIn(0, 100)
    progressBar.progress = progress
    
  3. 动画插值: 限制动画的起始或结束值,防止超出预期。
  4. 游戏开发: 限制玩家角色的移动范围,或限制敌人 AI 的行为范围。
  5. 数据验证: 在处理用户输入或从外部来源获取数据时,确保数值符合预期的业务规则。
    val quantity = inputString.toIntOrNull()?.coerceIn(1, 99) ?: 1 // 如果解析失败或超出范围,默认为1
    
  6. 数值计算: 避免计算结果超出合理的物理或逻辑限制。

coerceAtLeastcoerceAtMost

Kotlin 还提供了两个更细粒度的“强制”函数:

  • value.coerceAtLeast(minimumValue): 返回 valueminimumValue较大的那个。它只设定下限。
    val score = 80.coerceAtLeast(90) // 返回 90 (因为它不能低于 90)
    val score2 = 95.coerceAtLeast(90) // 返回 95
    
  • value.coerceAtMost(maximumValue): 返回 valuemaximumValue较小的那个。它只设定上限。
    val speed = 120.coerceAtMost(100) // 返回 100 (因为它不能高于 100)
    val speed2 = 90.coerceAtMost(100) // 返回 90
    

这两个函数在你只需要限制单边范围(只有上限或只有下限)时非常方便。

空安全

这一点是Kotlin的核心设计,也是它的一大卖点。Kotlin的空安全设计对于开发者来说是一种福利,它可以在编译阶段就发现很多空指针异常,而不是在运行时才发现。

作为一名 Android 开发者,你肯定深知 NullPointerException (NPE) 是 Java 开发中常见且令人头疼的问题。Kotlin 的设计目标之一就是消除这种运行时错误,通过在编译时强制进行空安全检查来解决这个问题。

1. 可空类型与非空类型

  • 非空类型 (Non-nullable Types): 默认情况下,Kotlin 中的所有类型都是非空的。这意味着你声明的变量如果没有明确标记为可空,就不能被赋值为 null。如果你尝试将 null 赋值给非空类型的变量,编译器会报错。
    var name: String = "Kotlin"
    // name = null // 编译错误:Null can not be a value of a non-null type String
    
  • 可空类型 (Nullable Types): 如果你确实需要一个变量可以持有 null 值,你必须在其类型后面加上问号 ? 来明确声明它为可空类型。
    var name: String? = "Kotlin"
    name = null // 编译通过
    

    这样,编译器就知道 name 这个变量可能为 null,并在你访问它的成员时强制你进行空检查。

Java里的空指针(NPE)报错,Kotlin 中也有类似的,就是使用 lateinit var 的不可空变量时,需要注意初始化和使用时机不对的情况下,有可能会报 UnInitializedPropertyAccessException 异常。所以在一些不确定是否在初始化完毕之后调用的方法里,使用变量时,最好加一层初始化判断。

// 延迟初始化变量
lateinit var str: String

// 使用时进行初始化判断
if(::str.isInitialized) {
    print(str.length)
}

2. 安全调用操作符 (?.)

当处理可空类型的变量时,你不能直接访问它的成员(例如调用方法或访问属性)。你需要使用安全调用操作符 ?.

  • 如果 ?. 左边的表达式不为 null,则会正常执行右边的操作。
  • 如果 ?. 左边的表达式为 null,则整个表达式的结果为 null,并且不会执行右边的操作,从而避免了 NPE。
    val name: String? = null
    val length: Int? = name?.length // 如果 name 为 null,则 length 也为 null
    println(length) // 输出: null
    
    val name2: String? = "Hello"
    val length2: Int? = name2?.length
    println(length2) // 输出: 5
    

3. Elvis 操作符 (?:)

Elvis 操作符 ?: 提供了一种简洁的方式来处理可空值,当左边的表达式为 null 时,提供一个默认值。

  • 如果 ?: 左边的表达式不为 null,则返回左边的值。
  • 如果 ?: 左边的表达式为 null,则返回 ?: 右边的默认值。
    val name: String? = null
    val length: Int = name?.length ?: 0 // 如果 name?.length 为 null,则 length 为 0
    println(length) // 输出: 0
    
    val name2: String? = "World"
    val length2: Int = name2?.length ?: 0
    println(length2) // 输出: 5
    

4. 非空断言操作符 (!!)

非空断言操作符 !! 允许你将任何可空类型的值转换为非空类型。然而,如果 !! 左边的表达式为 null,它会抛出一个 NullPointerException

val name: String? = null
// val length: Int = name!!.length // 运行时会抛出 NullPointerException

这个操作符应该慎用,只有当你非常确定某个值在特定时刻不可能为 null 时才使用。它的作用是告诉编译器“我保证这里不会是 null,如果错了,就让它崩溃吧”。

5. let 函数

let 是一个作用域函数,常用于对非空对象执行操作。如果接收者对象不为 nulllet 函数会执行给定的 lambda 表达式,并将接收者作为 it 参数传入。

val name: String? = "Kotlin"
name?.let {
    // 只有当 name 不为 null 时才执行这里的代码
    println("The name is ${it.toUpperCase()}")
}

val name2: String? = null
name2?.let {
    // 这段代码不会执行
    println("This will not be printed if name2 is null")
}

6. 安全类型转换 (as?)

安全类型转换 as? 尝试将一个值转换为指定的类型,如果转换失败,则返回 null,而不是抛出 ClassCastException

val obj: Any = "Hello"
val str: String? = obj as? String // str 为 "Hello"

val num: Any = 123
val str2: String? = num as? String // str2 为 null

平台类型 (Platform Types)

Kotlin 的空安全设计非常严格,但当它需要与 Java 代码交互时,就面临一个挑战。Java 不像 Kotlin 那样在类型系统中强制空安全,Java 的引用可以是 null,也可以是非 null,这在编译时是无法确定的。

为了解决这个问题,Kotlin 引入了平台类型 (Platform Types)

平台类型是指 Kotlin 编译器 无法确定其空性 的类型,通常是来自 Java 代码的类型。当你从 Java 代码中调用方法或访问字段时,Kotlin 编译器无法知道这些值是否可能为 null

平台类型在 Kotlin 中用 T! 的形式表示(例如 String!),但你不能在代码中显式地声明一个平台类型。它只会在编译器推断出类型时出现。

例如,如果你有一个 Java 类:

// JavaClass.java
public class JavaClass {
    public String getName() {
        return null; // Java 中可以返回 null
    }

    public void printValue(String value) {
        System.out.println(value.length()); // 如果 value 为 null,这里会抛出 NPE
    }
}

在 Kotlin 中使用 JavaClass

// Kotlin code
val javaClass = JavaClass()
val name = javaClass.name // name 的类型会被推断为 String! (平台类型)

平台类型的特点和处理

当你操作一个平台类型的值时,Kotlin 编译器不会强制进行空安全检查。这意味着你可以像在 Java 中那样使用它,但这也意味着你可能会遇到 NullPointerException,因为它可能在运行时为 null

对于平台类型,Kotlin 将空性的责任交给了开发者。你可以选择将其视为可空类型 (String?) 或非空类型 (String)。

  • 如果你确定它不会是 null,可以将其赋值给非空类型。如果运行时是 null,就会抛出 NPE。
  • 如果你不确定它是否为 null,最好将其赋值给可空类型,并使用安全调用操作符或其他空处理机制。

在Java代码中,为了帮助 Kotlin 编译器更好地理解 Java 代码的空性,Java 库可以使用空性注解(如 @Nullable, @NotNull,来自 JetBrains、AndroidX、JSR-305 等)。如果 Java 代码使用了这些注解,Kotlin 编译器可以根据注解信息 将 Java 类型映射为 Kotlin 的可空或非空类型 ,从而避免平台类型带来的不确定性。

总结来说,平台类型是 Kotlin 和 Java 互操作性中的一个“妥协点”,它允许你在 Kotlin 中使用 Java 代码,但同时也提醒你,在这些特定情况下,Kotlin 的编译时空安全保护可能会失效,你需要更加小心地处理潜在的 null 值。

单例类

Java中比较通用的单例类写法一般为static关键字声明的懒加载同步方法。

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // 私有构造函数,防止外部实例化
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在Kotlin中,想要定义一类为全局单例模式,只需要使用 object 关键字来声明类就可以了。

object Singleton {
    fun doSomething() {
        // 单例对象的方法
    }
}

// 使用
Singleton.doSomething()

这个写法等同于Java中的饿汉单例模式,对于开发者写起来更简洁,反编译之后的java代码如下:

public final class Singleton {
   @NotNull
   public static final Singleton INSTANCE = new Singleton();

   private Singleton() {
   }
}

伴生对象

在Kotlin中,每个类都可以有一个伴生对象。伴生对象的成员可以直接通过类名调用,而不需要实例化类。

class MyClass {
    companion object {
        fun doSomething() {
            // 伴生对象的方法
        }
    }
}

// 使用
MyClass.doSomething()

与Java中的静态方法类似,Kotlin中的伴生对象方法在反编译后的Java代码中也会被转换为静态方法。

在Android中,可以把类的TAG,和这个类强相关的一些常量,都定义在这个类的伴生对象中。

字符串模板

Java中,字符串和变量的结合需要使用加号+,而Kotlin中可以使用字符串模板来简化这个过程。在Kotlin中,可以使用字符串模板来动态构建字符串。字符串模板以$开头,在其中可以嵌入变量或表达式。

val name = "Kotlin"
val message = "Hello, $name!" // 字符串模板,结果为 "Hello, Kotlin!"

如果是和常量拼接,在编译器就会直接内联优化为字符串。如果是变量拼接,最后运行时实际上还是使用StringBuilder来拼接字符串。

扩展函数

在Kotlin中,可以为现有的类添加新的函数,而不需要修改类的源代码。这些新的函数被称为扩展函数。扩展函数允许你在不继承类的情况下,向类添加新的行为。

例如,String 类并没有一个内置的 isPalindrome() 方法来检查一个字符串是否是回文,但你可以通过扩展函数为它添加这个功能:

fun String.isPalindrome(): Boolean {
    val cleanedString = this.lowercase().replace(Regex("[^a-z0-9]"), "")
    return cleanedString == cleanedString.reversed()
}

fun main() {
    val word = "madam"
    println(word.isPalindrome()) // 输出: true
}

在这个例子中:

  • fun String.isPalindrome(): Boolean 定义了一个扩展函数。
  • String. 表示这个函数是 String 类的扩展。
  • 在函数内部,this 关键字引用了调用该函数的 String 实例。

扩展函数让代码看起来更自然。比如 string.isPalindrome()StringUtils.isPalindrome(string) 更直观。扩展函数可以把这些“工具”方法直接挂载到它们所操作的类上,减少了Utils工具类的数量,使得代码结构更清晰。

实现原理

Kotlin 的扩展函数实际上是一个静态函数。当 Kotlin 编译器处理扩展函数时,它会将其转换为一个普通的静态方法,这个静态方法会将接收者对象作为第一个参数。

例如,上面的 String.isPalindrome() 扩展函数在编译后,大致等价于一个 Java 中的静态方法:

// 编译后的伪 Java 代码
public final class StringExtensionsKt { // 自动生成的文件名,通常是文件名 + Kt
    public static final boolean isPalindrome(@NotNull String $receiver) {
        // 函数体内部的 this 对应于这里的 $receiver 参数
        String cleanedString = $receiver.toLowerCase().replaceAll("[^a-z0-9]", "");
        return cleanedString.equals(new StringBuilder(cleanedString).reverse().toString());
    }
}

然后,当你调用 word.isPalindrome() 时,编译器会将其转换为对这个静态方法的调用:

// 编译后的伪 Java 代码
StringExtensionsKt.isPalindrome(word);

这就是为什么扩展函数不能访问其接收者的 privateprotected 成员——因为它并不是真正意义上的成员函数,它只是一个方便的语法糖。

Android 开发中的常见应用

在 Android 开发中,扩展函数无处不在,极大地简化了代码:

  • View 扩展: 为 View 添加方便的函数,比如 View.show()View.hide()View.gone()
    fun View.show() {
        this.visibility = View.VISIBLE
    }
    
    fun View.hide() {
        this.visibility = View.INVISIBLE
    }
    
    fun View.gone() {
        this.visibility = View.GONE
    }
    
  • Context 扩展: 简化 Toast 显示、资源获取等操作。
    fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
        Toast.makeText(this, message, duration).show()
    }
    // 使用: context.toast("Hello!")
    
  • Fragment/Activity 扩展: 简化 FragmentTransactionIntent 的使用。
  • 数据类型转换: 比如为 IntLong 添加 toPx()toDp() 转换函数。

高阶函数 let、with、apply、run、also

也叫操作域函数,它们是 Kotlin 标准库中非常强大且常用的高阶函数。作为 Android 开发者,你肯定会在日常工作中频繁遇到和使用它们,因为它们能让你的代码更简洁、更易读,尤其是处理对象的配置、转换或安全调用时。

作用域函数是一种特殊的函数,它们的主要目的是在你提供的 lambda 表达式内部创建一个 临时作用域 。在这个作用域内,你可以直接访问(或引用)你所操作的对象,从而避免重复写对象名,让代码更紧凑。

Kotlin 提供了五种主要的作用域函数:let、run、with、apply 和 also。它们之间的主要区别在于:

  • 引用上下文对象的方式:使用 this 还是 it。
  • 返回值:返回上下文对象本身还是 lambda 表达式的结果。

apply

apply 函数,它的 lambda 表达式的 最后一行代码会自动作为返回值 返回。不同之处在于 apply 函数始终返回 上下文对象本身

val result = "Kotlin".apply {
    println("Length: $length") // 可以直接访问 String 的属性
}
// 输出: Length: 6
// 因为 apply 始终返回上下文对象本身,所以可以直接链式调用
"Kotlin".apply {
    println("Length: $length")
}.also {
    println("Also: $it") // 输出: Also: Kotlin
}

在Android中,apply通常用于对一个对象进行初始化或设置属性。例如,在RecyclerView的初始化过程中。

recyclerView.apply {
    layoutManager = LinearLayoutManager(context)
    addItemDecoration(MyDecoration(context)) // 添加分隔线装饰器
    setHasFixedSize(true) // 固定大小,提高性能
    adapter = myAdapter // 设置适配器
}

let

上面的空安全有提到一次,最常用于判空场景,非空后执行let中的代码。返回值 lambda 表达式的最后一行结果。还可以很方便地在链式调用中对结果进行操作或转换。

val name: String? = "Alice"

// 传统空检查
if (name != null) {
    println(name.length)
}

// 使用 let 进行空安全操作
name?.let {
    // 这里的 it 就是非空的 name
    println(it.length)
}

// 链式调用和转换
val result = "Hello Kotlin"
    .length
    .let { it * 2 } // 将长度乘以2
    .let { "Double length: $it" } // 转换为字符串
println(result) // 输出: Double length: 24

run

内部引用方式为 this ,返回值为 lambda 表达式的最后一行结果。

run 主要有两种应用形式:

作为扩展函数调用 (在对象上调用)

val user = User("Bob", 30)

val userDescription = user.run {
    // 这里的 this 就是 user 对象
    "Name: ${this.name}, Age: ${this.age}" // 返回这个字符串
}
println(userDescription) // 输出: Name: Bob, Age: 30

// 结合空安全 (类似 let)
val greeting: String? = "Hello"
val finalMessage = greeting?.run {
    // 这里的 this 就是非空的 "Hello"
    toUpperCase() + "!" // 返回 "HELLO!"
} ?: "No greeting" // 如果 greeting 为 null,则返回 "No greeting"
println(finalMessage) // 输出: HELLO!

独立调用

val message = run {
    val x = 10
    val y = 20
    "Sum: ${x + y}" // 返回这个字符串
}
println(message) // 输出: Sum: 30

with

引用方式为 this ,返回值 lambda 表达式的最后一行结果。 已知非空的对象执行一系列操作,而不需要链式调用。与 run 作为扩展函数类似,但 with 不是扩展函数写法,它将对象作为第一个参数传入。

val configuration = Configuration("Debug", 1024)

val configDetails = with(configuration) {
    // 这里的 this 就是 configuration 对象
    println("Configuring system...")
    "Mode: ${mode}, Size: ${maxSize}MB" // 返回这个字符串
}
println(configDetails) // 输出: Mode: Debug, Size: 1024MB

also

内部引用方式为 it 。返回 上下文对象本身 。主要用于 执行对象的附加操作,不影响对象本身,通常用于副作用 (side-effects)。例如,日志记录、调试输出或在对象准备好后执行一些不影响其状态的操作。

val numbers = mutableListOf(1, 2, 3)

val processedNumbers = numbers.also {
    // 这里的 it 就是 numbers 列表
    println("Before adding: $it") // 打印当前列表状态
    it.add(4)
}.also {
    println("After adding: $it") // 再次打印列表状态
}
// also 返回 numbers 列表本身,所以 processedNumbers 仍然是 numbers
println(processedNumbers) // 输出: [1, 2, 3, 4]

also 适用于你想在不改变原始对象的情况下,对其执行一些额外操作的场景。

操作域函数小结

  • let: 如果你想在代码块中对一个可空对象执行操作,或者想对结果进行转换,并返回转换后的值。
  • run:
    • 作为扩展函数: 如果你想配置一个对象并计算一个结果,或者结合空安全和 this 引用。
    • 作为非扩展函数: 如果你想封装一段语句,并返回其结果。
  • with: 如果你有一个非空对象,并且想在其作用域内执行一系列操作并返回一个结果
  • apply: 如果你想配置一个对象并返回该对象本身。非常适合链式设置多个属性。
  • also: 如果你想在不改变对象的情况下,对它执行一些额外操作或副作用(例如日志记录、调试打印),并返回该对象本身。
val originSting = "Kotlin"
val letString = originSting.let {
    it.uppercase()
}
val applyString = originSting.apply {
    uppercase()
}
val withString = with(originSting) {
    uppercase()
}
val runString = originSting.run {
    uppercase()
}
val alsoString = originSting.also {
    it.uppercase()
}
/**
letString: KOTLIN
applyString: Kotlin
withString: KOTLIN
runString: KOTLIN
alsoString: Kotlin
*/

根据运行结果可以看出,apply和also都是返回操作的对象本身的,另外的三个,都是返回最后一行表达式的结果。在使用操作域函数时,需要注意这一点以免拿到不符合预期的数据。

map扩展函数

map 可以对List,Map,Set等集合对象中的元素进行转换,生成一个新的集合。

例如:

val numbers = listOf(1, 2, 3, 4)
val doubled = numbers.map { it * 2 } 
// 结果: [2, 4, 6, 8]

// 或显式命名参数
val squared = numbers.map { number -> number * number }
// 结果: [1, 4, 9, 16]

在Android中,也可以用在初始化要显示的数据集上,语法更简洁:

private val functionList = listOf(
    "壁纸取色测试" to {
        startActivity(Intent(this, WallpaperTestActivity::class.java))
    },
    "弹一个Toast" to {
        Toast.makeText(this, "一个普通的Toast", Toast.LENGTH_SHORT).show()
    },
    "设备Root状态" to {
        startActivity(Intent(this, RootInfoActivity::class.java))
    },
    "CPU信息" to {
        startActivity(Intent(this, CpuInfoActivity::class.java))
    },
).map { (name, task) -> FunctionItem(name, task) }

还可以和 Flow 数据流一起作用,在数据发送之前使用 map 预处理一遍:

fun mapTest() {
    CoroutineScope(Dispatchers.IO).launch {
        flowOf(1, 2, 3, 4, 5).map {
            it + 1
        }.collectLatest {
            Log.i(TAG, "mapTest collect $it")
        }
    }
}

use扩展函数

use 函数是 Kotlin 标准库为实现了 Closeable 或 AutoCloseable 接口的类(如 FileInputStream、BufferedReader 等)提供的扩展函数。

它主要用于​资源管理​​(如文件、网络连接、数据库连接等),它可以确保资源在使用完毕后被正确关闭,即使发生异常也能保证资源释放,防止内存泄露。其底层实现实际上也是对try-catch-finally的封装。

用法举例,独取一个文件的内容:

fun readFile() {
    val file = File("example.txt")
    FileInputStream(file).use { inputStream ->
        val bytes = inputStream.readBytes()
        println(String(bytes))
    } // inputStream 自动关闭
}

数据库连接:

fun queryDatabase() {
    val connection: Connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db")
    connection.use { conn ->
        // 执行 SQL 查询
        conn.createStatement().use { statement ->
            statement.executeQuery("SELECT * FROM users").use { resultSet ->
                while (resultSet.next()) {
                    println(resultSet.getString("name"))
                }
            }
        }
    } // conn 自动关闭
}

Kotlin集合

对于Kotlin和Java中的集合简要对比,专门提取来一篇来记录:

Kotlin中的集合

data class数据类

data class 是 Kotlin 中的一个重要概念,通常用于表示数据对象。

数据类是专门为存储数据而设计的类。 Kotlin 编译器会自动为数据类生成许多有用的成员函数,从而省去了你手动编写这些函数的麻烦。这使得你的代码更简洁、更安全、更易读。

在 Java 中,为了实现一个简单的数据持有类,你需要写大量的模板代码(构造函数、getter/setter、equals()hashCode()toString() 等),而 Kotlin 的数据类用一个关键字就搞定了这一切。

data class User(val name: String, val age: Int)
  1. 所有属性的 Getters (以及 var 属性的 Setters): 尽管在 Kotlin 中我们通常直接访问属性,但底层它们依然是存在的。

    val user = User("Alice", 30)
    println(user.name) // 访问 name 属性
    
  2. equals(other: Any?): 用于比较两个数据类对象是否相等。当且仅当两个对象的类型相同,并且主构造函数中声明的所有属性的值都相等时,它们才被认为是相等的。

    val user1 = User("Alice", 30)
    val user2 = User("Alice", 30)
    val user3 = User("Bob", 25)
    
    println(user1 == user2) // 输出: true (因为属性值相同)
    println(user1 == user3) // 输出: false
    

    注意: 传统的类比较的是内存地址(引用相等),而数据类比较的是内容(结构相等)。

  3. hashCode(): 返回一个基于主构造函数中所有属性的哈希码。这在将数据类对象存储在哈希集合(如 HashSetHashMap)中时至关重要。equals()hashCode() 必须保持一致性(如果两个对象 equals 返回 true,它们的 hashCode 也必须相同)。

    val userSet = hashSetOf(user1)
    println(userSet.contains(user2)) // 输出: true (因为 user2 的 equals 和 hashCode 与 user1 相同)
    
  4. toString(): 返回一个包含类名和所有属性及其值的字符串表示。这对于日志记录和调试非常有用。

    val user = User("Alice", 30)
    println(user) // 输出: User(name=Alice, age=30)
    
  5. componentN() 函数: 为每个在主构造函数中声明的属性生成一个 componentN() 函数,其中 N 是属性在声明时的顺序(component1() 对应第一个属性,component2() 对应第二个,以此类推)。这些函数使得数据类可以支持解构声明 (Destructuring Declarations)

    val (name, age) = User("Alice", 30) // 解构声明
    println("Name: $name, Age: $age") // 输出: Name: Alice, Age: 30
    
  6. copy(): 创建一个新对象,复制现有对象的所有属性,同时允许你选择性地修改某些属性的值。这对于创建对象的一个副本但需要轻微修改时非常有用,因为数据类通常是不可变的(尽管也可以有 var 属性)。

    val originalUser = User("Alice", 30)
    val copiedUser = originalUser.copy(age = 31) // 复制 originalUser,只改变 age 属性
    val anotherCopiedUser = originalUser.copy(name = "Bob") // 改变 name 属性
    
    println(originalUser)    // 输出: User(name=Alice, age=30)
    println(copiedUser)      // 输出: User(name=Alice, age=31)
    println(anotherCopiedUser) // 输出: User(name=Bob, age=30)
    

数据类使用注意事项

  1. 主构造函数必须至少有一个参数:所有自动生成的函数都是基于主构造函数中声明的属性。
  2. 主构造函数的所有参数都必须标记为 valvar:这是为了确保它们是类中的属性,而不是仅仅是构造函数参数。
  3. 不能是 abstractopensealedinner:数据类通常是最终的,不适合继承层次结构。
  4. 可以有其他成员: 除了自动生成的函数,你也可以在数据类中定义自己的函数、属性或伴生对象。

    data class Product(val id: String, val name: String, var price: Double) {
        // 自定义函数
        fun displayInfo() {
            println("Product ID: $id, Name: $name, Price: $price")
        }
    
        // 伴生对象
        companion object {
            const val DEFAULT_CURRENCY = "USD"
        }
    }
    
  5. 属性的默认值: 你可以为数据类的主构造函数属性提供默认值。

    data class Settings(val theme: String = "dark", val notificationsEnabled: Boolean = true)
    val defaultSettings = Settings() // 使用默认值
    val customSettings = Settings(theme = "light") // 覆盖默认值
    

Android中常用场景

在 Android 开发中,数据类无处不在:

  • API 响应模型: 当你从 RESTful API 获取数据时,通常会定义数据类来映射 JSON 或 XML 结构。
    data class Post(val userId: Int, val id: Int, val title: String, val body: String)
    
  • 数据库实体: 当使用 Room Persistence Library 或其他 ORM 框架时,数据类可以很好地表示数据库表中的一行数据。
    @Entity(tableName = "users")
    data class UserEntity(@PrimaryKey val id: Long, val name: String, val email: String)
    
  • UI 状态: 在 MVVM 或 MVI 架构中,数据类常用于表示 UI 的当前状态,方便进行状态的更新和比较。
    data class UserProfileState(
        val isLoading: Boolean = false,
        val user: User? = null,
        val errorMessage: String? = null
    )
    
  • 事件 (Events): 在事件驱动的架构中,数据类可以很好地表示各种事件。
    sealed class LoginEvent {
        data class Success(val userId: String) : LoginEvent()
        data class Error(val message: String) : LoginEvent()
        object Loading : LoginEvent()
    }
    

Kotlin高级特性

sealed class和sealed interface

Kotlin 的密封类 (Sealed Class) 是一个非常棒的特性,尤其是在处理有限的、受限的类继承结构时。它能让你的代码更安全、更具表达力,并且在与 when 表达式结合使用时,能提供强大的编译时检查。

密封类是一种限制类继承层次结构的特殊抽象类。 它的主要目的是声明一个受限的类层次结构,其中所有可能的子类都必须在同一文件内声明(Kotlin 1.5 之后可以在同一个模块内的任何文件中声明,但通常仍推荐在同一文件内以保持紧凑性)。

这就意味着,编译器在编译时就知道了这个密封类的所有可能直接子类。这种“已知子类”的特性是密封类最有价值的地方。

// 定义一个密封类来表示网络请求的结果
sealed class NetworkResult {
    data class Success(val data: String) : NetworkResult() // 子类可以是数据类
    data class Error(val message: String) : NetworkResult()   // 子类可以是数据类
    object Loading : NetworkResult()                          // 子类可以是单例对象
    class Idle : NetworkResult()                              // 子类也可以是普通类
}

在这个例子中:

  • NetworkResult 是一个密封类。
  • SuccessErrorLoadingIdleNetworkResult 的直接子类。
  • 重要: 所有的这些子类都必须在定义 NetworkResult同一文件内(或者在 Kotlin 1.5+ 中,在同一模块内),这样编译器才能“知道”它们。

1. 确保穷举性检查 (Exhaustiveness Checking) 与 when 表达式

这是密封类最强大的特性。当你在 when 表达式中使用密封类的实例时,如果 when 覆盖了所有可能的子类型,Kotlin 编译器会强制你处理所有可能的子类,并且不需要 else 分支。如果遗漏了某个子类,编译器会报错,从而防止运行时错误。

fun handleNetworkResult(result: NetworkResult) {
    when (result) {
        is NetworkResult.Success -> {
            println("数据加载成功: ${result.data}")
        }
        is NetworkResult.Error -> {
            println("加载失败: ${result.message}")
        }
        NetworkResult.Loading -> { // 注意:对于 object,直接引用即可
            println("正在加载中...")
        }
        is NetworkResult.Idle -> {
            println("网络请求处于空闲状态。")
        }
        // 不需要 else 分支,因为编译器知道所有可能的子类型都被处理了
    }
}

这对于构建健壮的应用程序至关重要,特别是在处理 UI 状态、事件或网络响应时。

2. 更好的类型安全和代码可读性

密封类提供了一种清晰的方式来建模有限的状态。例如,一个 UI 组件的状态可能只有“加载中”、“显示数据”或“显示错误”几种。使用密封类可以明确地表示这些状态,使得代码的意图一目了然,并减少了引入无效状态的可能性。

3. 作为枚举的替代(更强大)

虽然枚举 (enum class) 也能表示一组有限的值,但枚举的每个成员都是一个简单的实例,不能携带额外的状态。而密封类的每个子类可以是独立的类,可以拥有自己的属性和行为,这使得它比枚举更加灵活和强大。

// 枚举无法携带额外数据
enum class Color { RED, GREEN, BLUE }

// 密封类可以携带额外数据
sealed class Shape {
    data class Circle(val radius: Double) : Shape()
    class Square(val side: Double) : Shape()
    object Triangle : Shape() // 也可以是无状态的单例
}

密封类与枚举 (Enum Class) 的场景选择

  • 使用枚举: 如果你只需要表示一组固定且不携带额外数据的常量集(例如方向:上、下、左、右;或简单的状态:开启、关闭)。
  • 使用密封类: 如果你需要表示一组有限的、可携带不同数据或具有不同行为的子类型(例如网络请求结果、UI 状态、事件)。

密封类在 Android 开发中的常见应用

在 Android 开发中,密封类几乎无处不在,是管理复杂状态和事件的利器:

  1. 网络请求结果: 如上面示例所示,表示 API 调用的不同状态(成功、失败、加载中)。
    sealed class Resource<out T> { // 可以是泛型
        data class Success<out T>(val data: T) : Resource<T>()
        data class Error(val message: String, val errorCode: Int) : Resource<Nothing>()
        object Loading : Resource<Nothing>()
    }
    
  2. UI 状态: 定义一个屏幕可能拥有的所有状态。
    sealed class UserViewState {
        object Loading : UserViewState()
        data class Loaded(val user: User) : UserViewState()
        data class Error(val errorMessage: String) : UserViewState()
        object Empty : UserViewState()
    }
    
  3. 用户交互事件: 表示用户在界面上的各种操作。
    sealed class ProfileEvent {
        object LoadProfile : ProfileEvent()
        data class UpdateName(val newName: String) : ProfileEvent()
        object Logout : ProfileEvent()
    }
    
  4. RecyclerView 列表项: 如果一个 RecyclerView 可以显示不同类型的列表项(Header, Item, Footer),可以用密封类来建模。

协程

Kotlin的协程也是广为开发者讨论的一个异步框架,在Android应用开发过程中,几乎可以完全替代线程的使用,并且以同步方式写异步代码看起来也比较优雅。

详细的有多篇文章介绍过:

Kotlin协程的基础使用

Kotlin协程浅谈

Kotlin协程的取消与异常处理

Kotlin协程挂起恢复源码解析

内联函数 (Inline Functions) 与交叉内联 (Crossinline)/无内联 (Noinline)

关于这几个内联相关的关键字,由另一篇文章也记录过:

Kotlin的inline&crossinline&noinline关键字

泛型的 in out 和 Reified 关键字

Kotlin协变和逆变

委托

在 Kotlin 中,委托(Delegation) 是一种强大的设计模式,它允许对象将部分功能委托给另一个辅助对象来实现。Kotlin 原生支持多种委托方式,主要分为以下几种:

类委托(Class Delegation)

通过 by 关键字,将类的接口实现委托给另一个对象,常用于 “装饰器模式” 或 “代理模式”。

示例:委托接口实现

interface Printer {
    fun print(message: String)
}

class DefaultPrinter : Printer {
    override fun print(message: String) {
        println("Default Printer: $message")
    }
}

// 委托给 printer 对象
class CustomPrinter(private val printer: Printer) : Printer by printer {
    // 可以覆盖部分方法
    override fun print(message: String) {
        println("Before Printing...")
        printer.print(message) // 调用委托对象的方法
        println("After Printing...")
    }
}

fun main() {
    val defaultPrinter = DefaultPrinter()
    val customPrinter = CustomPrinter(defaultPrinter)
    customPrinter.print("Hello, Kotlin!")
}

输出:

Before Printing… Default Printer: Hello, Kotlin! After Printing…

适用场景:

  • 增强或修改现有类的行为(如日志、缓存、权限控制)。
  • 避免继承,使用组合代替。

属性委托(Property Delegation)

Kotlin 提供标准库委托(如 lazy、observable),也可以自定义委托。

(1) lazy 延迟初始化

val lazyValue: String by lazy {
    println("Computed only once!")
    "Hello"
}

fun main() {
    println(lazyValue) // 第一次访问时计算
    println(lazyValue) // 直接返回缓存值
}

输出:

Computed only once! Hello Hello

(2) observable 监听属性变化

import kotlin.properties.Delegates

var observedValue: Int by Delegates.observable(0) { _, old, new ->
    println("Value changed from $old to $new")
}

fun main() {
    observedValue = 10  // 触发回调
    observedValue = 20  // 再次触发
}

输出:

Value changed from 0 to 10 Value changed from 10 to 20

(3) vetoable 可拦截修改

var positiveNumber: Int by Delegates.vetoable(0) { _, old, new ->
    new > 0  // 只有 new > 0 时才允许修改
}

fun main() {
    positiveNumber = 10  // 允许
    println(positiveNumber)  // 10
    positiveNumber = -5     // 拒绝修改
    println(positiveNumber)  // 仍然是 10
}

(4) 自定义属性委托

class StringDelegate(private var initValue: String) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("Getting value: $initValue")
        return initValue
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("Setting value: $value")
        initValue = value
    }
}

fun main() {
    var text by StringDelegate("Default")
    println(text)  // 调用 getValue
    text = "New Value"  // 调用 setValue
}

输出:

Getting value: Default Default Setting value: New Value

【Kotlin】协程的取消中断与异常处理

【Kotlin】协程的取消中断与异常处理

本文介绍了Kotlin协程的取消机制和异常处理方案

文章源码和介绍来自Kotlin官方网站

协程的取消

在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,一个用户也许关闭了一个启动了协程的界面,那么现在协程的执行结果已经不再被需要了,这时,它应该是可以被取消的。

launch 函数返回了一个可以被用来取消运行中的协程的 Job:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancel() // 取消该作业
    job.join() // 等待作业执行结束
    println("main: Now I can quit.")
//sampleEnd
}

程序执行后的输出如下:

job: I’m sleeping 0 … job: I’m sleeping 1 … job: I’m sleeping 2 … main: I’m tired of waiting! main: Now I can quit.

一旦 main 函数调用了 job.cancel,我们在其它的协程中就看不到任何输出,因为它被取消了。 这里也有一个可以使 Job 挂起的函数 cancelAndJoin 它合并了对 cancel 以及 join 的调用。

取消是协作的

协程的取消是 协作 的。一段协程代码必须协作才能被取消。 所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException。 然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例代码所示:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
            // 每秒打印消息两次
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 等待一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消一个作业并且等待它结束
    println("main: Now I can quit.")
//sampleEnd
}

运行示例代码,并且我们可以看到它连续打印出了“I’m sleeping”,甚至在调用取消后, 作业仍然执行了五次循环迭代并运行到了它结束为止。

The same problem can be observed by catching a CancellationException and not rethrowing it:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = launch(Dispatchers.Default) {
        repeat(5) { i ->
            try {
                // print a message twice a second
                println("job: I'm sleeping $i ...")
                delay(500)
            } catch (e: Exception) {
                // log the exception
                println(e)
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")
//sampleEnd    
}

While catching Exception is an anti-pattern, this issue may surface in more subtle ways, like when using the runCatching function, which does not rethrow CancellationException.

使计算代码可取消

我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取消。对于这种目的 yield 是一个好的选择。 另一种方法是显式的检查取消状态。让我们试试第二种方法。

将前一个示例中的 while (i < 5) 替换为 while (isActive) 并重新运行它。

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // 可以被取消的计算循环
            // 每秒打印消息两次
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 等待一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并等待它结束
    println("main: Now I can quit.")
//sampleEnd
}

你可以看到,现在循环被取消了。isActive 是一个可以被使用在 CoroutineScope 中的扩展属性。

在 finally 中释放资源

我们通常使用如下的方法处理在被取消时抛出 CancellationException 的可被取消的挂起函数。比如说,try {……} finally {……} 表达式以及 Kotlin 的 use 函数一般在协程被取消的时候执行它们的终结动作:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            println("job: I'm running finally")
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并且等待它结束
    println("main: Now I can quit.")
//sampleEnd
}

join 和 cancelAndJoin 等待了所有的终结动作执行完毕, 所以运行示例得到了下面的输出:

job: I’m sleeping 0 … job: I’m sleeping 1 … job: I’m sleeping 2 … main: I’m tired of waiting! job: I’m running finally main: Now I can quit.

运行不能取消的代码块

在前一个例子中任何尝试在 finally 块中调用挂起函数的行为都会抛出 CancellationException,因为这里持续运行的代码是可以被取消的。通常,这并不是一个问题,所有良好的关闭操作(关闭一个文件、取消一个作业、或是关闭任何一种通信通道)通常都是非阻塞的,并且不会调用任何挂起函数。然而,在真实的案例中,当你需要挂起一个被取消的协程,你可以将相应的代码包装在 withContext(NonCancellable) {……} 中,并使用 withContext 函数以及 NonCancellable 上下文,见如下示例所示:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并等待它结束
    println("main: Now I can quit.")
//sampleEnd
}

超时

在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件事。 来看看示例代码:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
//sampleEnd
}

运行后得到如下输出:

I’m sleeping 0 … I’m sleeping 1 … I’m sleeping 2 … Exception in thread “main” kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

withTimeout 抛出了 TimeoutCancellationException,它是 CancellationException 的子类。 我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。 然而,在这个示例中我们在 main 函数中正确地使用了 withTimeout。

由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会超时的代码包装在 try {…} catch (e: TimeoutCancellationException) {…} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // 在它运行得到结果之前取消它
    }
    println("Result is $result")
//sampleEnd
}

运行这段代码时不再抛出异常:

I’m sleeping 0 … I’m sleeping 1 … I’m sleeping 2 … Result is null

异步超时和资源

withTimeout中的超时事件对于在其代码块中运行的代码来说是异步的,可以在任何时间发生,甚至可以在从超时代码块内部返回之前发生。如果您在代码块内打开或获取了某些资源,而这些资源需要在代码块外关闭或释放,请记住这一点。

例如,在这里我们用 Resource 类模仿了一个可关闭的资源,该类只需通过在其关闭函数中递增获取计数器和递减计数器来记录创建次数。现在,让我们创建大量的例行程序,每个例行程序都在 withTimeout 代码块末尾创建一个资源,并在代码块外释放该资源。我们添加了一个小延迟,这样就更有可能在 withTimeout 代码块已经完成时发生超时,从而导致资源泄漏。

import kotlinx.coroutines.*

//sampleStart
var acquired = 0

class Resource {
    init { acquired++ } // Acquire the resource
    fun close() { acquired-- } // Release the resource
}

fun main() {
    runBlocking {
        repeat(10_000) { // Launch 10K coroutines
            launch { 
                val resource = withTimeout(60) { // Timeout of 60 ms
                    delay(50) // Delay for 50 ms
                    Resource() // Acquire a resource and return it from withTimeout block     
                }
                resource.close() // Release the resource
            }
        }
    }
    // Outside of runBlocking all coroutines have completed
    println(acquired) // Print the number of resources still acquired
}
//sampleEnd

如果运行上述代码,你会发现它并不总是打印零值,不过这可能取决于你的机器的定时。你可能需要调整本示例中的超时时间,才能真正看到非零值。

需要注意的是,在这里通过 10K 例程对获取的计数器进行递增和递减是完全线程安全的,因为它总是在同一个线程(即 runBlocking 所使用的线程)中进行。关于这一点的更多解释,将在 “例程上下文 ”一章中进行。

要解决这个问题,可以在变量中存储对资源的引用,而不是从 withTimeout 代码块中返回。

import kotlinx.coroutines.*

var acquired = 0

class Resource {
    init { acquired++ } // Acquire the resource
    fun close() { acquired-- } // Release the resource
}

fun main() {
//sampleStart
    runBlocking {
        repeat(10_000) { // Launch 10K coroutines
            launch { 
                var resource: Resource? = null // Not acquired yet
                try {
                    withTimeout(60) { // Timeout of 60 ms
                        delay(50) // Delay for 50 ms
                        resource = Resource() // Store a resource to the variable if acquired      
                    }
                    // We can do something else with the resource here
                } finally {  
                    resource?.close() // Release the resource if it was acquired
                }
            }
        }
    }
    // Outside of runBlocking all coroutines have completed
    println(acquired) // Print the number of resources still acquired
//sampleEnd
}

本例始终打印 0。资源不会泄漏。

协程的异常处理

本节内容涵盖了异常处理与在异常上取消。 我们已经知道被取消的协程会在挂起点抛出 CancellationException 并且它会被协程的机制所忽略。在这里我们会看看在取消过程中抛出异常或同一个协程的多个子协程抛出异常时会发生什么。

异常的传播

协程构建器有两种形式:自动传播异常(launch)或向用户暴露异常(async 与 produce)。 当这些构建器用于创建一个根协程时,即该协程不是另一个协程的子协程, 前者这类构建器将异常视为未捕获异常,类似 Java 的 Thread.uncaughtExceptionHandler, 而后者则依赖用户来最终消费异常,例如通过 await 或 receive(produce 与 receive 的相关内容包含于通道章节)。

可以通过一个使用 GlobalScope 创建根协程的简单示例来进行演示:

GlobalScope 是一种微妙的应用程序接口,可能会产生非同小可的反作用。为整个应用程序创建根例行程序是 GlobalScope 罕见的合法用途之一,因此您必须通过 @OptIn(DelicateCoroutinesApi::class)明确选择使用 GlobalScope。

import kotlinx.coroutines.*

//sampleStart
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val job = GlobalScope.launch { // launch 根协程
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // 我们将在控制台打印 Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async { // async 根协程
        println("Throwing exception from async")
        throw ArithmeticException() // 没有打印任何东西,依赖用户去调用等待
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}
//sampleEnd

这段代码的输出如下(调试):

Throwing exception from launch Exception in thread “DefaultDispatcher-worker-1 @coroutine#2” java.lang.IndexOutOfBoundsException Joined failed job Throwing exception from async Caught ArithmeticException

CoroutineExceptionHandler将未捕获异常打印到控制台的默认行为是可自定义的。

根协程中的 CoroutineExceptionHandler 上下文元素可以被用于这个根协程通用的 catch 块,及其所有可能自定义了异常处理的子协程。

它类似于 Thread.uncaughtExceptionHandler 。

你无法从 CoroutineExceptionHandler 的异常中恢复。当调用处理者的时候,协程已经完成并带有相应的异常。通常,该处理者用于记录异常,显示某种错误消息,终止和(或)重新启动应用程序。

CoroutineExceptionHandler 仅在未捕获的异常上调用 — 没有以其他任何方式处理的异常。 特别是,所有子协程(在另一个 Job 上下文中创建的协程)委托它们的父协程处理它们的异常,然后它们也委托给其父协程,以此类推直到根协程, 因此永远不会使用在其上下文中设置的 CoroutineExceptionHandler。 除此之外,async 构建器始终会捕获所有异常并将其表示在结果 Deferred 对象中, 因此它的 CoroutineExceptionHandler 也无效。

在监督作用域内运行的协程不会将异常传播到其父协程,并且会从此规则中排除。本文档的另一个小节——监督提供了更多细节。

import kotlinx.coroutines.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("CoroutineExceptionHandler got $exception") 
    }
    val job = GlobalScope.launch(handler) { // 根协程,运行在 GlobalScope 中
        throw AssertionError()
    }
    val deferred = GlobalScope.async(handler) { // 同样是根协程,但使用 async 代替了 launch
        throw ArithmeticException() // 没有打印任何东西,依赖用户去调用 deferred.await()
    }
    joinAll(job, deferred)
//sampleEnd    
}

这段代码的输出如下:

CoroutineExceptionHandler got java.lang.AssertionError

取消与异常

取消与异常紧密相关。协程内部使用 CancellationException 来进行取消,这个异常会被所有的处理者忽略,所以那些可以被 catch 代码块捕获的异常仅仅应该被用来作为额外调试信息的资源。 当一个协程使用 Job.cancel 取消的时候,它会被终止,但是它不会取消它的父协程。

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = launch {
        val child = launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                println("Child is cancelled")
            }
        }
        yield()
        println("Cancelling child")
        child.cancel()
        child.join()
        yield()
        println("Parent is not cancelled")
    }
    job.join()
//sampleEnd    
}

这段代码的输出如下:

Cancelling child Child is cancelled Parent is not cancelled

如果一个协程遇到了 CancellationException 以外的异常,它将使用该异常取消它的父协程。 这个行为无法被覆盖,并且用于为结构化的并发(structured concurrency) 提供稳定的协程层级结构。 CoroutineExceptionHandler 的实现并不是用于子协程。

在这些示例中,CoroutineExceptionHandler 总是被设置在由 GlobalScope 启动的协程中。将异常处理者设置在 runBlocking 主作用域内启动的协程中是没有意义的,尽管子协程已经设置了异常处理者, 但是主协程也总是会被取消的。

当父协程的所有子协程都结束后,原始的异常才会被父协程处理, 见下面这个例子。

import kotlinx.coroutines.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("CoroutineExceptionHandler got $exception") 
    }
    val job = GlobalScope.launch(handler) {
        launch { // 第一个子协程
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(100)
                    println("The first child finished its non cancellable block")
                }
            }
        }
        launch { // 第二个子协程
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
//sampleEnd 
}

这段代码的输出如下:

Second child throws an exception Children are cancelled, but exception is not handled until all children terminate The first child finished its non cancellable block CoroutineExceptionHandler got java.lang.ArithmeticException

异常聚合

当协程的多个子协程因异常而失败时, 一般规则是“取第一个异常”,因此将处理第一个异常。 在第一个异常之后发生的所有其他异常都作为被抑制的异常绑定至第一个异常。

import kotlinx.coroutines.*
import java.io.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE) // 当另一个同级的协程因 IOException  失败时,它将被取消
            } finally {
                throw ArithmeticException() // 第二个异常
            }
        }
        launch {
            delay(100)
            throw IOException() // 首个异常
        }
        delay(Long.MAX_VALUE)
    }
    job.join()  
}

这段代码的输出如下:

CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]

注意,这个机制当前只能在 Java 1.7 以上的版本中使用。 在 JS 和原生环境下暂时会受到限制,但将来会取消。

取消异常是透明的,默认情况下是未包装的:

import kotlinx.coroutines.*
import java.io.*

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val job = GlobalScope.launch(handler) {
        val innerJob = launch { // 该栈内的协程都将被取消
            launch {
                launch {
                    throw IOException() // 原始异常
                }
            }
        }
        try {
            innerJob.join()
        } catch (e: CancellationException) {
            println("Rethrowing CancellationException with original cause")
            throw e // 取消异常被重新抛出,但原始 IOException 得到了处理
        }
    }
    job.join()
//sampleEnd    
}

这段代码的输出如下:

Rethrowing CancellationException with original cause CoroutineExceptionHandler got java.io.IOException

监督

正如我们之前研究的那样,取消是在协程的整个层次结构中传播的双向关系。让我们看一下需要单向取消的情况。

此类需求的一个良好示例是在其作用域内定义作业的 UI 组件。如果任何一个 UI 的子作业执行失败了,它并不总是有必要取消(有效地杀死)整个 UI 组件, 但是如果 UI 组件被销毁了(并且它的作业也被取消了),由于其结果不再需要了,因此有必要取消所有子作业。

另一个例子是服务进程孵化了一些子作业并且需要 监督 它们的执行,追踪它们的故障并在这些子作业执行失败的时候重启。

SupervisorJob

SupervisorJob 可以用于这些目的。 它类似于常规的 Job,唯一的不同是:SupervisorJob 的取消只会向下传播。这是很容易用以下示例演示:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        // 启动第一个子作业——这个示例将会忽略它的异常(不要在实践中这么做!)
        val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
            println("The first child is failing")
            throw AssertionError("The first child is cancelled")
        }
        // 启动第二个子作业
        val secondChild = launch {
            firstChild.join()
            // 取消了第一个子作业且没有传播给第二个子作业
            println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                // 但是取消了监督的传播
                println("The second child is cancelled because the supervisor was cancelled")
            }
        }
        // 等待直到第一个子作业失败且执行完成
        firstChild.join()
        println("Cancelling the supervisor")
        supervisor.cancel()
        secondChild.join()
    }
//sampleEnd
}

这段代码的输出如下:

The first child is failing The first child is cancelled: true, but the second one is still active Cancelling the supervisor The second child is cancelled because the supervisor was cancelled

监督作用域

对于作用域的并发,可以用 supervisorScope 来替代 coroutineScope 来实现相同的目的。它只会单向的传播并且当作业自身执行失败的时候将所有子作业全部取消。作业自身也会在所有的子作业执行结束前等待, 就像 coroutineScope 所做的那样。

import kotlin.coroutines.*
import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    try {
        supervisorScope {
            val child = launch {
                try {
                    println("The child is sleeping")
                    delay(Long.MAX_VALUE)
                } finally {
                    println("The child is cancelled")
                }
            }
            // 使用 yield 来给我们的子作业一个机会来执行打印
            yield()
            println("Throwing an exception from the scope")
            throw AssertionError()
        }
    } catch(e: AssertionError) {
        println("Caught an assertion error")
    }
//sampleEnd
}

这段代码的输出如下:

The child is sleeping Throwing an exception from the scope The child is cancelled Caught an assertion error

监督协程中的异常

常规的作业和监督作业之间的另一个重要区别是异常处理。 监督协程中的每一个子作业应该通过异常处理机制处理自身的异常。 这种差异来自于子作业的执行失败不会传播给它的父作业的事实。 这意味着在 supervisorScope 内部直接启动的协程确实使用了设置在它们作用域内的 CoroutineExceptionHandler,与父协程的方式相同 (参见 CoroutineExceptionHandler 小节以获知更多细节)。

import kotlin.coroutines.*
import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("CoroutineExceptionHandler got $exception") 
    }
    supervisorScope {
        val child = launch(handler) {
            println("The child throws an exception")
            throw AssertionError()
        }
        println("The scope is completing")
    }
    println("The scope is completed")
//sampleEnd
}

这段代码的输出如下:

The scope is completing The child throws an exception CoroutineExceptionHandler got java.lang.AssertionError The scope is completed

【Kotlin】inline&crossinline&noinline关键字

【Kotlin】inline&crossinline&noinline关键字

本文介绍了Compose的重组流程,主要是最小重组范围的界定和优化

来自扔物线朱凯大佬的博客学习笔记

JVM常量编译时优化

Kotlin中,使用了 const val 关键字修饰的变量,在编译时会被视为常量,并且在编译时进行了优化。直接将其值复制到调用处,而不是像普通变量一样在运行时进行变量访问。这可以提高代码的执行效率,因为避免了变量调用的开销。

const val CONST_VAL = 10

fun main() {
    println(CONST_VAL)
}

// 编译后
fun main() {
    println(10)
}

inline 内联函数

编译时同样被提前处理的还有内联函数,即使用了 inline 关键字修饰的函数。

JVM在编译时,会将inline函数内的代码直接复制到调用处,而不是像普通函数一样在运行时进行函数调用。听起来可能会对性能有优化,实际上少一层函数调用栈的优化是非常微小的。

而同时, 函数内联 不同于 常量内联 的地方在于,函数体通常比常量复杂多了,而函数内联会导致函数体被拷贝到每个调用处,如果函数体比较大而被调用处又比较多,就会导致编译出的字节码变大很多。

lambda参数实现方式

在Kotlin中,lambda参数的实现方式是使用了 匿名内部类 ,而不是使用了 函数指针

在编译之后,可以看到lambda参数调用的地方,实际上是Kotlin帮我们生成了一个匿名内部类,然后在调用处调用这个匿名内部类的方法。

class LambdaTest {
    fun testInline(lambdaParams:()->Unit) {
        lambdaParams()
    }
}

经过反编译成Java代码之后:

public final class LambdaTest {
   @NotNull
   public final LambdaTest testInline(@NotNull Function0 lambdaParams) {
      Intrinsics.checkNotNullParameter(lambdaParams, "lambdaParams");
      lambdaParams.invoke();
      return this;
   }
}

可以看到,lambdaParams的类型是 Function0 ,这是一个接口。在运行过程中,就会生成一个匿名内部类,然后在调用处调用这个匿名内部类的方法。

inline对lambda的优化

如果上述的testinline方法,在外部被高频循环调用。

fun main() {
    val lambdaTest = LambdaTest()
    for (i in 0..100000) {
        lambdaTest.testInline {
            println("hello world")
        }
    }
}

内存占用会蹭的一下涨上来。

如果使用了这个接收lambda参数的方法使用了 inline 关键字修饰,就不会生成匿名内部类,而是直接将lambda的代码块里面的代码复制到调用处。

inline 关键字不止可以内联自己的内部代码,还可以内联自己内部的内部的代码,意思是什么呢,就是你的函数在被加了 inline 关键字之后,编译器在编译时不仅会把函数内联过来,而且会把它内部的函数类型的参数——那就是那些 Lambda 表达式——也内联过来。换句话说,这个函数被编译器贴过来的时候是完全展开铺平的:

kotlin源代码:

class LambdaTest {
    inline fun testInline(lambdaParams:()->Unit) {
        lambdaParams()
    }
}

fun main() {
    val lambdaTest = LambdaTest()
    for (i in 0..100000) {
        lambdaTest.testInline {
            println("hello world")
        }
    }
}

反编译之后:

public final class LambdaTest {
   public final void testInline(@NotNull Function0 lambdaParams) {
      Intrinsics.checkNotNullParameter(lambdaParams, "lambdaParams");
      lambdaParams.invoke();
   }
}

public final class MainKt {
   public static final void main() {
      LambdaTest lambdaTest = new LambdaTest();
      int $i$iv = 0;
      int var3;
      for(var3 = 100000; $i$iv <= var3; ++$i$iv) {
         System.out.println("hello world");
      }
   }
}

高阶函数(Higher-order Functions)有它们天然的性能缺陷,我们通过 inline 关键字让函数用内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题。

inline另类用法

在kotlin的 UMath.kt 工具类中,有一个max方法:

@SinceKotlin("1.5")
@WasExperimental(ExperimentalUnsignedTypes::class)
@kotlin.internal.InlineOnly
public inline fun max(a: UInt, b: UInt): UInt {
    return maxOf(a, b)
}

这个maxOf方法,来自于另一个工具类 UComparisonsKt

@SinceKotlin("1.5")
@WasExperimental(ExperimentalUnsignedTypes::class)
public fun maxOf(a: UInt, b: UInt): UInt {
    return if (a >= b) a else b
}

这里就通过内联的方式,将maxOf方法的代码块内联到了调用处。

可以直接通过方便的顶层函数的方式,来使用工具类,不需要创建实例或者带外部类名。

noinline

inline 是内联,而 noinline 就是不内联。不过它不是作用于函数的,而是作用于函数的参数:对于一个标记了 inline 的内联函数,你可以对它的任何一个或多个函数类型的参数添加 noinline 关键字。添加了之后,这个参数就不会参与内联。

函数类型的参数,它本质上是个对象。我们可以把这个对象当做函数来调用,这也是最常见的用法。但同时我们也可以把它当做对象来用。比如把它当做返回值:

inline fun testInline(lambdaParams:()->Unit) {
    lambdaParams()
    return lambdaParams
}

但当我们把函数进行内联的时候,它内部的这些参数就不再是对象了,因为他们会被编译器拿到调用处去展开。

当一个函数被内联之后,它内部的那些函数类型的参数就不再是对象了,因为它们的壳被脱掉了。换句话说,对于编译之后的字节码来说,这个对象根本就不存在。一个不存在的对象,你怎么使用?

所以当你要把一个这样的参数当做对象使用的时候,Android Studio 会报错,告诉你这没法编译

noinline 就是用来局部地、指向性地关掉函数的内联优化的。既然是优化,为什么要关掉?因为这种优化会导致函数中的函数类型的参数无法被当做对象使用,也就是说,这种优化会对 Kotlin 的功能做出一定程度的收窄。而当你需要这个功能的时候,就要手动关闭优化了。这也是 inline 默认是关闭、需要手动开启的另一个原因:它会收窄 Kotlin 的功能。

crossinline

inline 函数将 Lambda 参数传递给另一个执行上下文(如另一个函数、另一个线程、协程或其他作用域)时,为了防止非局部返回,必须使用 crossinline

保持 Lambda 的内联优化,但禁止在 Lambda 内部使用裸奔的 return 关键字(即非局部返回)。它确保 Lambda 只能使用标签返回 (return@label)隐式返回。使用 crossinline 确保内联函数的行为符合预期,避免 Lambda 内部的 return 意外地跳出外部的非内联函数。

看这样一个情景:

一个内联函数,接受一个 lambda 参数。

inline fun lambdaReturnTest(insertAction: () -> Unit) {
    insertAction()
}

如果在调用处,lambda参数里带一个return:

override fun onCreate() {
    super.onCreate()

    Log.i("sdvgsrhbTAG", "before erftgyujhf")
    lambdaReturnTest {
        println("Hello World")
        return
    }
    Log.i("sdvgsrhbTAG", "after erftgyujhf")
}

这时候结束的不是这个lambdaReturnTest方法,而是onCreate方法。因为lambdaReturnTest方法被内联了,会直接铺平展开到调用处,连带里面的return。

这样的话,我们每次在lambda里面使用return还需要确认这个函数是否是内联函数,才可以确认这个return结束的是哪一个函数。为此Kotlin规定 不允许在lambda参数中使用return,除非这个使用lambda参数的函数是内联函数

那这样的话规则就简单了:

  • Lambda 里的 return,结束的不是直接的外层函数,而是外层再外层的函数;
  • 但只有内联函数的 Lambda 参数可以使用 return。

目前的Kotlin版本其实也可以在return后面使用\@来指明返回的哪一级的函数。

示例:异步或嵌套执行

假设您有一个 safeRun 函数,它在一个内部(非内联)的 Runnable 中执行您的 Lambda。

// 内部非内联函数,它接受一个普通 Lambda/Runnable
fun executeInExecutor(block: () -> Unit) {
    // 实际的 Android/Java 场景可能是:Executor.execute(Runnable { ... })
    println("任务被包装并排队...")
    block() // 模拟执行
}

// 场景:创建一个安全的执行块,但其中的任务会被传递到另一个函数中执行
inline fun safeRun(crossinline block: () -> Unit) {
    println("--- 准备执行 ---")
    // 如果这里没有 crossinline,编译器无法保证 block() 不会被非局部返回跳出 safeRun 之外
    executeInExecutor {
        // block 的代码在这里被执行
        block() 
    }
    println("--- 执行完毕 ---")
}

fun main() {
    fun callSafeRun() {
        safeRun {
            println("开始任务")
            // return // ❌ 编译错误:禁止非局部返回
            return @ safeRun // ✅ 允许:只能使用标签返回,只跳出 safeRun 
        }
        println("callSafeRun 结束")
    }
    
    callSafeRun() 
}

/* 输出:
--- 准备执行 ---
任务被包装并排队...
开始任务
--- 执行完毕 ---
callSafeRun 结束
*/

如果没有 crossinline,Lambda { return } 理论上可以执行非局部返回,直接跳出 callSafeRun 函数。但由于 Lambda 实际是在非内联的 executeInExecutor 内部执行的,这种行为是不允许的,因此 crossinline 强制阻止了非局部返回,以保证程序的控制流是清晰且安全的。

双层嵌套的lambda场景

inline fun lambdaReturnTest(insertAction: () -> Unit) {
    doubleLambda { insertAction() }
}

fun doubleLambda(insertAction: () -> Unit) {
    insertAction()
}

doubleLambda方法是一个普通函数,非内联函数,它的参数是一个函数类型的参数。

如果像这样带两层lambda调用,那么其中使用return就又会无法判断结束的到底是哪一层函数。 这里Kotlin是直接禁止了这种写法。

如果确实要有这种间接调用需求,那么可以使用crossinline来解决。当你给一个需要被间接调用的参数加上 crossinline,就对它进行了局部加强内联,相当于insertAction还是会被展开铺平到调用处,解除了这个限制,从而就可以对它进行双层间接调用了。

但是又会有return结束层级不确定性,所以Kotlin规定了使用了crossinline的函数,不能在lambda参数中使用return。

只能二选一了。

总结

结论就是:

  • inline 可以让你用内联——也就是函数内容直插到调用处——的方式来优化代码结构,从而减少函数类型的对象的创建;
  • noinline 是局部关掉这个优化,来摆脱 inline 带来的「不能把函数类型的参数当对象使用」的限制;
  • crossinline 是局部加强这个优化,让内联函数里的函数类型的参数可以被当做对象使用。

【Kotlin】协程的基础使用

【Kotlin】协程的基础使用

本文介绍Kotlin协程挂起和恢复的原理

文章后半部分源码和介绍来自Kotlin官方网站

协程简介

协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。协程 是在 1.3 版中添加到 Kotlin 的,基于既定的从其他语言转换成的概念。

Android 上,协程有助于管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致应用无响应。使用协程的专业开发者中有超过 50% 的人反映使用协程提高了工作效率。本主题介绍如何使用 Kotlin 协程解决以下问题,从而让您能够编写出更清晰、更简洁的应用代码。

协程和线程

线程

  • 线程是操作系统级别的概念
  • 我们开发者通过编程语言(Thread.java)创建的线程,本质还是操作系统内核线程的映射
  • JVM 中的线程与内核线程的存在映射关系,有“一对一”,“一对多”,“M对N”。* JVM 在不同操作系统中的具体实现会有差别,“一对一”是主流
  • 一般情况下,我们说的线程,都是内核线程,线程之间的切换,调度,都由操作系统负责
  • 线程也会消耗操作系统资源,但比进程轻量得多
  • 线程,是抢占式的,它们之间能共享内存资源,进程不行
  • 线程共享资源导致了多线程同步问题
  • 有的编程语言会自己实现一套线程库,从而能在一个内核线程中实现多线程效果,早期 JVM 的“绿色线程” 就是这么做的,这种线程被称为“用户线程”

协程

  • 协程不是操作系统级别的概念,无需操作系统支持
  • 协程有点像上面提到的“绿色线程”,一个线程上可以运行成千上万个协程
  • 协程是用户态的(userlevel),内核对协程无感知
  • 协程是协作式的,由开发者管理,不需要操作系统进行调度和切换,也没有抢占式的消耗,因此它更加高效
  • 协程它底层基于状态机实现,多协程之间共用一个实例,资源开销极小,因此它更加轻量
  • 协程本质还是运行于线程之上,它通过协程调度器,可以运行到不同的线程上

项目使用实例

最常见的使用方式,在 ViewModel 或者 Controller 里写业务逻辑,在 Activity 里调用,这样就可以在IO线程执行网络请求,拿到结果后自动切换到主线程更新UI。

// viewModel或者controller里获取数据逻辑
// 使用suspend限制在协程里使用;withContext切换调度器,指定在IO线程执行下面的任务
suspend fun getUserName() = withContext(Dispatchers.IO) {
    debugLog("thread name: ${Thread.currentThread().name}")
    ServiceCreator.createService<UserService>()
        .getUserName("2cd1e3c5ee3cda5a")
        .execute()
        .body()
}

// Activity调用处
override fun onCreate(savedInstanceState: Bundle?){
    // 最直接的声明方法,在主线程执行下面的逻辑
    lifeCycleScope.launch {
        // 相当于get这一半是在IO线程执行
        //拿到结果后的变量赋值这一半操作由调度器自动切换到主线程来执行了
        val userName = mViewModel.getUserName()
        infoLog("userName: $userName")
        binding.tvUserName.text = userName
    }
}

API介绍

四个基础概念

  • suspend function。即挂起函数,delay() 就是协程库提供的一个用于实现非阻塞式延时的挂起函数
  • CoroutineScope。即协程作用域,GlobalScope 是 CoroutineScope 的一个实现类,用于指定协程的作用范围,可用于管理多个协程的生命周期,所有协程都需要通过 CoroutineScope 来启动
  • CoroutineContext。即协程上下文,包含多种类型的配置参数。Dispatchers.IO 就是 CoroutineContext 这个抽象概念的一种实现,用于指定协程的运行载体,即用于指定协程要运行在哪类线程上
  • CoroutineBuilder。即协程构建器,协程在 CoroutineScope 的上下文中通过 launch、async 等协程构建器来进行声明并启动。launch、async 均被声明为 CoroutineScope 的扩展方法

Kotlin 协程(Coroutines)提供了一套丰富的 API 方法,用于简化异步编程。以下是一些常用的 API 方法及其简要说明:

启动

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
}

start参数代表启动方式:

CoroutineStart.DEFAULT:协程创建后,立即开始调度,但 有可能在执行前被取消。在调度前如果协程被取消,其将直接进入取消响应的状态。 CoroutineStart.LAZY:只要协程被需要时(主动调用该协程的 start、 join、 await等函数时 ), 才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态。 CoroutineStart.ATOMIC:协程创建后,立即开始调度, 协程执行到第一个挂起点之前不响应取消。其将调度和执行两个步骤合二为一,就像它的名字一样,其保证调度和执行是原子操作,因此协程也 一定会执行。 CoroutineStart.UNDISPATCHED:协程创建后,立即在当前线程中执行,直到遇到第一个真正挂起的点。是立即执行,因此协程 一定会执行。

context上下文参数:

Job:工作空间。用于启动or取消协程。

Dispatchers为调度器。用于指定协程的执行线程。 Default:默认调度器 ,适合处理后台计算,其是一个 CPU 密集型任务调度器。 IO:IO 调度器,适合执行 IO 相关操作,其是 IO 密集型任务调度器。 Main:UI 调度器,根据平台不同会被初始化为对应的 UI 线程的调度器, 在Android 平台上它会将协程调度到 UI 事件循环中执行,即通常在 主线程上执行。 Unconfined:“无所谓”调度器,不要求协程执行在特定线程上。 CoroutineExceptionHandler:全局异常捕获(只能在根协程配置)。

CoroutineName:协程名称。

协程上下文就是CoroutineContext,其中可以用加和函数plus()来连接使用,比如:

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job + handler

这里的+就是加和函数,如上所写就是让CoroutineContext具备主线程+工作空间job,和CoroutineExceptionHandler的能力。

作用域

  • 顶级作用域:GlobalScope–> 全局范围,不会自动结束执行,无法取消。
  • 协同作用域:coroutineScope –> 抛出异常会取消父协程
  • 主从作用域:supervisorScope –> 抛出异常,不会取消父协程

三种作用域真正常用的其实只有主从作用域,谁也不想让自己写的协程挂了导致app崩溃吧。但实际使用过程中,由于没有作用域的概念,往往会用到顶级作用域和协同作用域,协程挂了导致app崩溃,然后再去解决异常。

常用的主从作用域有下面这些:

  • MainScope :主线程的作用域,全局范围,可以取消。
  • lifecycleScope : 生命周期范围,用于activity等有生命周期的组件,在Desroyed的时候会自动结束。
  • viewModelScope :ViewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束。

主从作用域启动的协程,崩溃后不会影响其他协程执行。

以MainScope为例,在构建上下文时,加入了SupervisorJob(),SupervisorJob()是一个工作空间,它会在子协程抛出异常时,会将异常控制在子协程内部,不往上传递,不会影响父协程的执行。

线程切换

还是以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
}

追进start方法:

    /**
     * Starts this coroutine with the given code [block] and [start] strategy.
     * This function shall be invoked at most once on this coroutine.
     * 
     * - [DEFAULT] uses [startCoroutineCancellable].
     * - [ATOMIC] uses [startCoroutine].
     * - [UNDISPATCHED] uses [startCoroutineUndispatched].
     * - [LAZY] does nothing.
     */
    public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
        start(block, receiver, this)
    }

可以看到模式启动模式下,使用的是 startCoroutineCancellable ,最终会调用到 resumeCancellableWith 方法,在 resumeCancellableWith 方法中,会判断当前上下文是否需要重新分发,如果需要就将上下文中提取新的Dispathers赋给dispatcher,否则就在当前线程直接执行。

inline fun resumeCancellableWith(
        result: Result<T>,
        noinline onCancellation: ((cause: Throwable) -> Unit)?
    ) {
        val state = result.toState(onCancellation)
        // 判断当前上下文是否需要重新分发,如果需要就将上下文中提取新的Dispathers赋给dispatcher,否则就在当前线程直接执行
        if (dispatcher.isDispatchNeeded(context)) {
            _state = state
            resumeMode = MODE_CANCELLABLE
            dispatcher.dispatch(context, this)
        } else {
            executeUnconfined(state, MODE_CANCELLABLE) {
                if (!resumeCancelled(state)) {
                    resumeUndispatchedWith(result)
                }
            }
        }
    }

在不同的JVM平台上,Dispatcher.Main 调度器的执行位置取决于 具体的UI框架 。以下是主要情况:

  • Android平台上会调度到Android的主线程(UI线程)执行,这是通过Handler(Looper.getMainLooper())实现的
  • JavaFX平台会调度到JavaFX的Application线程执行,这是通过Platform.runLater()实现的
  • Swing平台会调度到Swing的Event Dispatch Thread (EDT)执行,这是通过SwingUtilities.invokeLater()实现的
  • 其他情况则会回退到单线程执行器

除了主调度器之外,其他几个切换也类似此流程,比如Dispatchers.Default是 创建了一个默认的线程池 ,而Dispatchers.IO也是沿用的线程池,只是对线程数量做了限制罢了。

IOS平台

在iOS平台上,Kotlin协程的线程切换主要通过以下方式实现:

  1. Main Dispatcher(主线程调度器): 使用DispatchQueue.main来调度到主线程执行 这是通过Kotlin/Native与iOS的GCD(Grand Central Dispatch)集成实现的 协程会被调度到主队列(Main Queue)执行,确保UI操作在主线程进行
  2. Default Dispatcher(默认调度器): 使用后台线程池执行任务 在iOS上,这通常是通过GCD的全局队列(Global Queue)实现的 使用DispatchQueue.global()来获取后台队列
  3. IO Dispatcher(IO调度器): 专门用于IO密集型操作 同样基于GCD实现,但使用不同的队列优先级 使用DispatchQueue.global(qos: .utility)或DispatchQueue.global(qos: .background)来执行IO操作

简化api表达:

// Main Dispatcher实现
internal class MainDispatcher : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        DispatchQueue.main.async {
            block.run()
        }
    }
}

// Default Dispatcher实现
internal class DefaultDispatcher : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        DispatchQueue.global().async {
            block.run()
        }
    }
}

流程图如下:

常用api

协程构建器

用于启动协程的主要方法。

  • launch
    启动一个不会返回结果的协程(Job 类型)。
    GlobalScope.launch {
        // 协程代码
    }
    
  • async
    启动一个会返回结果的协程(Deferred 类型),结果可以通过 await() 获取。
    val deferred = GlobalScope.async {
        // 协程代码
        "Result"
    }
    val result = deferred.await()
    
  • runBlocking
    阻塞当前线程,直到协程执行完毕。通常用于测试或主函数中。
    runBlocking {
        // 协程代码
    }
    

协程上下文与调度器

用于控制协程的执行线程或上下文。

  • Dispatchers.Default
    用于 CPU 密集型任务的默认线程池。
    launch(Dispatchers.Default) {
        // 在后台线程执行
    }
    
  • Dispatchers.IO
    用于 IO 密集型任务的线程池。
    launch(Dispatchers.IO) {
        // 执行 IO 操作
    }
    
  • Dispatchers.Main
    用于在主线程(如 Android 的 UI 线程)执行任务。
    launch(Dispatchers.Main) {
        // 更新 UI
    }
    
  • Dispatchers.Unconfined
    不限制协程的执行线程,根据调用点决定。
    launch(Dispatchers.Unconfined) {
        // 不限制线程
    }
    
  • withContext
    切换协程的上下文。
    withContext(Dispatchers.IO) {
        // 在 IO 线程执行
    }
    

协程作用域

用于管理协程的生命周期。

  • GlobalScope
    全局作用域,协程的生命周期与应用程序一致。
    GlobalScope.launch {
        // 全局协程
    }
    
  • CoroutineScope
    自定义作用域,通常与 lifecycleScopeviewModelScope 结合使用。
    val scope = CoroutineScope(Dispatchers.Main)
    scope.launch {
        // 协程代码
    }
    
  • lifecycleScope(Android)
    Lifecycle 绑定的作用域,协程在 Lifecycle 销毁时自动取消。
    lifecycleScope.launch {
        // 协程代码
    }
    
  • viewModelScope(Android)
    ViewModel 绑定的作用域,协程在 ViewModel 销毁时自动取消。
    viewModelScope.launch {
        // 协程代码
    }
    

协程取消与超时

用于控制协程的执行时间或取消协程。

  • cancel()
    取消协程。
    val job = launch {
        // 协程代码
    }
    job.cancel()
    
  • isActive
    检查协程是否仍处于活动状态。
    if (isActive) {
        // 协程仍在运行
    }
    
  • withTimeout
    设置协程的超时时间,超时后抛出 TimeoutCancellationException
    withTimeout(1000) {
        // 协程代码
    }
    
  • withTimeoutOrNull
    设置协程的超时时间,超时后返回 null 而不是抛出异常。
    val result = withTimeoutOrNull(1000) {
        // 协程代码
    }
    

协程挂起函数

用于在协程中挂起执行。

  • delay
    挂起协程一段时间。
    delay(1000) // 挂起 1 秒
    
  • yield
    挂起当前协程,让出执行权给其他协程。
    yield()
    

协程异常处理

用于处理协程中的异常。

  • try-catch
    捕获协程中的异常。
    try {
        // 协程代码
    } catch (e: Exception) {
        // 处理异常
    }
    
  • CoroutineExceptionHandler
    全局异常处理器。
    val handler = CoroutineExceptionHandler { _, exception ->
        // 处理异常
    }
    launch(handler) {
        // 协程代码
    }
    

协程组合与并发

用于处理多个协程的组合与并发。

  • awaitAll
    等待多个 Deferred 完成并返回结果列表。
    val deferred1 = async { 1 }
    val deferred2 = async { 2 }
    val results = awaitAll(deferred1, deferred2)
    
  • supervisorScope
    创建一个子作用域,子协程的失败不会影响其他子协程。
    supervisorScope {
        launch {
            // 子协程 1
        }
        launch {
            // 子协程 2
        }
    }
    
  • coroutineScope
    创建一个子作用域,子协程的失败会传播到父协程。
    coroutineScope {
        launch {
            // 子协程 1
        }
        launch {
            // 子协程 2
        }
    }
    

协程间的通信Channel

编写具有共享可变状态的代码非常困难且容易出错(例如在使用回调的解决方案中)。更简单的方法是通过通信而不是使用公共可变状态来共享信息。协程可以通过通道相互通信。

通道是允许数据在协程之间传递的通信原语。 一个协程可以向通道发送一些信息,而另一个协程可以从该通道接收该信息

使用方法

发送(生产)信息的协程通常称为生产者,接收(消费)信息的协程称为消费者。一个或多个协程可以向同一个通道发送信息,一个或多个协程也可以从该通道接收数据。

当多个协程从同一个通道接收信息时,每个元素仅由其中一个消费者处理一次。 一旦元素被处理,它将立即从通道中移除。

可以将通道视为元素集合,或者更准确地说,队列这种数据结构,其中元素被添加到一端并从另一端接收。但是,有一个重要的区别:与集合不同,即使在其同步版本中,通道也可以暂停 send()和receive()操作。当通道为空或满时会发生这种情况。如果通道大小有上限,则通道可能会满。

Channel由三个不同的接口表示:SendChannel、ReceiveChannel和Channel,其中后者扩展了前两个。您通常会创建一个通道并将其作为SendChannel实例提供给生产者,以便只有他们可以向该通道发送信息。

您将通道作为ReceiveChannel实例提供给消费者,以便只有他们可以从中接收信息。send和receive方法都声明为suspend:

interface SendChannel<in E> {
    suspend fun send(element: E)
    fun close(): Boolean
}

interface ReceiveChannel<out E> {
    suspend fun receive(): E
}

interface Channel<E> : SendChannel<E>, ReceiveChannel<E>

生产者可以关闭一个通道来表明没有更多的元素到来。

库中定义了几种类型的通道。它们的区别在于内部可以存储多少个元素以及是否send()可以暂停调用。对于所有通道类型,receive()调用的行为都类似:如果通道不为空,则接收一个元素;否则,调用将被暂停。

创建通道时,请指定其类型或缓冲区大小(如果需要缓冲):

val rendezvousChannel = Channel<String>()
val bufferedChannel = Channel<String>(10)
val conflatedChannel = Channel<String>(CONFLATED)
val unlimitedChannel = Channel<String>(UNLIMITED)

默认情况下,会创建一个“Rendezvous”通道。

在以下任务中,您将创建一个“Rendezvous”通道、两个生产者协程和一个消费者协程:

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val channel = Channel<String>()
    launch {
        channel.send("A1")
        channel.send("A2")
        log("A done")
    }
    launch {
        channel.send("B1")
        log("B done")
    }
    launch {
        repeat(3) {
            val x = channel.receive()
            log(x)
        }
    }
}

fun log(message: Any?) {
    println("[${Thread.currentThread().name}] $message")
}

Flow API

Flow API 是 Kotlin 协程库中的一部分,主要用于处理数据流。

  • flow
    创建一个冷流(Cold Flow)。
    val flow = flow {
        emit(1)
        emit(2)
    }
    
  • collect
    收集流中的数据。
    flow.collect { value ->
        // 处理数据
    }
    
  • map
    对流中的数据进行转换。
    flow.map { value -> value * 2 }
    
  • filter
    过滤流中的数据。
    flow.filter { value -> value > 1 }
    
  • flatMapConcat
    将流中的每个值映射为一个新流,并按顺序连接。
    flow.flatMapConcat { value -> flowOf(value, value * 2) }
    
  • zip
    将两个流合并为一个流。
    val flow1 = flowOf(1, 2)
    val flow2 = flowOf("A", "B")
    flow1.zip(flow2) { a, b -> "$a$b" }
    

关于更多Flow的基础和进阶使用,此前也写过更详细的一篇文章。

Kotlin Flow全面总结

Pagination