0%

runBlocking 的阻塞实现原理是 Kotlin 协程基础架构中非常精妙的一部分。它并不是通过简单的“忙等待”(busy-wait)来实现阻塞,而是通过在当前线程上启动一个事件循环(Event Loop) 来实现的。

下面我们分层来解析它的实现原理。

核心原理:线程局部事件循环 (Thread-Local Event Loop)

当调用 runBlocking 时,它的核心工作是在当前线程上安装并运行一个事件循环。这个事件循环负责调度和执行其代码块内所有的协程任务,直到所有任务完成。

1. 阻塞的入口:runBlocking 调用

当你调用 runBlockBlocking { ... } 时,会发生以下几步:

a. 创建事件循环和调度器

  • runBlocking 会创建一个 BlockingEventLoop 作为当前线程的局部事件循环。

  • 同时,它会创建一个 BlockingCoroutine 实例,它使用这个新创建的事件循环作为其协程上下文中的 ContinuationInterceptor(调度器)。

b. 启动协程

  • 你传入的 lambda 代码块被包装成一个协程(StandaloneCoroutine),并在这个新创建的、使用阻塞事件循环的上下文中启动。

c. 进入事件循环

  • 这是最关键的一步。runBlocking 会调用 eventLoop.join() 或类似的机制(在旧版本中是 joinBlocking)。

  • 这个 join() 方法会启动一个 while 循环,这个循环会持续运行,直到满足两个条件:

    1. 事件循环中没有可立即执行的任务(协程)。

    2. 事件循环已经退出isCompleted 为 true),即所有子协程都已完成。

2. 事件循环的内部工作方式

这个 while 循环就是“阻塞”发生的地方,但它内部是高效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 概念上的伪代码,简化自 kotlinx.coroutines.EventLoopImplBase
private fun joinBlocking() {
val eventLoop = ThreadLocalEventLoop.current() as BlockingEventLoop
while (true) {
// 1. 检查是否所有工作都已完成
if (eventLoop.isCompleted) break // 条件2满足,退出循环,解除阻塞

// 2. 处理所有队列中的任务
val task = eventLoop.processNextEvent()
if (task != null) {
// 执行任务(例如,恢复一个挂起的协程)
task.run()
} else {
// 3. 如果没有立即要处理的任务,但事件循环还未结束
// 就让线程真正地“睡眠”一小段时间,而不是忙等待
// 在JVM上,这可能会调用 LockSupport.parkNanos()
eventLoop.parkNanos()
}
}
}
  • 有任务时:循环会不断地从事件队列中取出任务(例如,一个 delay 定时器到期了,需要恢复某个协程)并执行它。此时线程是在“工作”的。

  • 无任务且未完成时:线程会通过 parkNanos 等方法被短暂挂起,避免 100% 的 CPU 占用。一旦有新任务被提交到事件循环(例如,一个定时器到期),线程会被唤醒并继续工作。

3. 如何结束阻塞?

当 runBlocking 代码块内部的所有协程(包括所有子协程)都执行完毕后,根协程的状态会变为“已完成”。这会触发事件循环将自己的 isCompleted 标志设置为 true

外层的 while 循环检测到这个条件后,就会 break 退出循环。runBlocking 方法随之返回,调用线程得以继续执行后面的代码。阻塞解除

与挂起 (Suspend) 的关键区别

为了更好理解,我们对比一下 runBlocking 和普通协程构建器(如 launch)的区别:

特性 runBlocking launch / async
线程行为 阻塞 (Block) 挂起 (Suspend)
实现机制 在当前线程启动一个事件循环并运行 while 循环。 将协程体包装成状态机,通过 Continuation.resumeWith() 在调度器线程池中执行。
资源占用 占用调用线程,但通过事件循环高效调度,无任务时线程会短暂休眠。 不占用调用线程。当协程挂起时,底层线程立即被释放,可去执行其他任务。
使用场景 主函数、测试、集成阻塞代码与协程世界。 常规的异步、并发编程。
  • 挂起:是协程的行为。一个挂起函数释放了它当前占用的线程,当结果准备好时,它会在另一个线程(由调度器决定)上恢复执行。

  • 阻塞:是线程的行为。一个阻塞操作占用了线程,在操作完成之前,这个线程不能做任何其他事情。runBlocking 的“阻塞”是阻塞在它自己的事件循环上。

总结:runBlocking 的阻塞原理

  1. 安装事件循环runBlocking 在当前线程上设置了一个专属于它的 **BlockingEventLoop**。

  2. 运行循环至完成:它启动一个 while 循环,这个循环会持续运行,处理事件循环中的任务(恢复协程、执行定时操作等)。

  3. 高效等待:循环在没有立即任务但协程未全部完成时,会通过 park 等操作让线程短暂休眠,避免CPU空转。

  4. 退出条件:一旦它内部的所有协程工作完成,循环退出,runBlocking 返回,线程解除阻塞。

所以,runBlocking 的阻塞是一种有生产力的阻塞——线程虽然被“卡”在了一个循环里,但这个循环正在高效地驱动着整个协程世界的运转。这正是它不能用在已有协程环境中的原因,因为你会把一个本该用于处理大量协程的线程(如 Dispatchers.IO 中的线程)浪费在运行这一个事件循环上。

由Deepseek生成,已理解。

获取trace文件

1、获取ANR日志 adb bugreport,导出trace文件,分析trace, pid, tid

2、解压日志后/data/anr路径下根据发生时间选择对应文件

3、—– pid 17172 at 2025-07-08 15:53:02 —–

根据当前pid分析出17172进程出现问题并且记录发生时间

分析ANR

  1. 通过进程ID17172搜索定位到
1
2
3
4
5
6
"main" prio=5 tid=1 Native 
| group="main" sCount=1 dsCount=0 flags=1 obj=0x7261bba8 self=0x7e35a97c00
| sysTid=17472 nice=-10 cgrp=default sched=0/0 handle=0x7e37005ed0
| state=S schedstat=( 13473368664 317028758 13398 ) utm=1066 stm=281 core=5 HZ=100
| stack=0x7fdb9c2000-0x7fdb9c4000 stackSize=8192KB
| held mutexes=
  1. 解释字段含义
    “main” prio=5 tid=1 Native
    1
    2
    3
    4
    "main":线程名称(主线程)。 
    prio=5:线程优先级(5 是默认优先级,范围 1~10,值越大优先级越高)。
    tid=1:线程 ID(1 通常是主线程)。
    Native:线程当前在执行 本地代码(JNI/NDK),而非 Java/Kotlin 代码。

group=”main” sCount=1 dsCount=0 flags=1 obj=0x7261bba8 self=0x7e35a97c00

1
2
3
4
5
6
group="main":线程所属组(主线程组)。 
sCount=1:线程被挂起(suspend)的次数。
dsCount=0:调试器挂起次数(0 表示未被调试器暂停)。
flags=1:线程状态标志(1 通常表示线程处于活跃状态)。
obj=0x7261bba8:线程关联的 Java 对象地址。
self=0x7e35a97c00:线程本身的 Native 地址。

sysTid=17172 nice=-10 cgrp=default sched=0/0 handle=0x7e37005ed0

1
2
3
4
5
sysTid=17172:系统级线程 ID(与 ps -t 命令看到的 ID 一致)。 
nice=-10:线程的调度优先级(-20~19,值越小优先级越高,0 是默认值)。
cgrp=default:线程所属的 CPU 调度组(默认组)。
sched=0/0:调度策略(0 表示 SCHED_NORMAL,即普通优先级)。
handle=0x7e37005ed0:线程句柄的内存地址。

state=S schedstat=( 13473368664 317028758 13398 ) utm=1066 stm=281 core=5 HZ=100

1
2
3
4
5
state=S:线程当前状态(S 表示休眠/Sleeping,其他常见状态:R=运行中,D=不可中断休眠)。 
schedstat=( 13473368664 317028758 13398 ):调度统计信息,格式为 (运行时间ns, 等待时间ns, 执行次数)。
utm=1066 stm=281:线程在用户态(utm)和内核态(stm)的累计运行时间(单位:jiffies,1 jiffy=10ms)。
core=5:线程最后运行的 CPU 核心编号。
HZ=100:系统时钟频率(100 表示每秒 100 次 tick)。
字段 当前值 正常参考值 问题指示
nice -10 0 异常提高优先级,需审查代码
schedstat 13.47秒运行 <100ms 主线程重度占用 CPU
utm/stm 10.66s/2.81s <1s 存在未异步化的耗时任务

人际交往的基本技巧

1. 如果想采蜂蜜,就不要捅蜂窝

动物对那些它们认为是正确的行为的学习会十分迅速,而且记得更加牢固。这远比通过惩罚它们的错误行为来教育它们更为迅速有效。

人们厌恶批评就如同渴望认可一样。

批评带来的怨恨会打击员工的积极性,影响你和家人、朋友的感情。同时,你所批评的境况仍得不到改善。

己所不欲,勿施于人。

2. 每个人都渴望得到赞美

所有和我们共事的人都是普普通通的人,他们都渴望得到肯定。每个灵魂都渴望获得赞美。

普天之下,只有一个方法可以让别人做事,那就是让别人想做这件事。

每个人对赞美的渴望,正如同对美食的渴望一样强烈。

我们日常最容易忽略的一种美德就是赞赏别人。

真心地给予别人肯定,不要吝啬你的赞美。人们会为你的肯定和赞美而欢呼雀跃,并且会用心铭记你的话语,直到永远。

3. 激起别人急切的欲望

如果成功有什么秘诀的话,那就是要先去了解对方的需求,从他们的角度来思考问题。首先激起对方急切的欲望。能做到这点的人,将掌握世界;不能做到这点的人,将四处碰壁。

唯一能影响别人的方法就是跟他们谈论那些他们想要的东西,并让他们知道怎么样才能得到它。

让别人喜欢你的六种方法

1. 用热情和真诚的态度对待他人

一个人可以通过真诚地关心他人来赢得关注、时间,甚至是合作。

如果你希望别人喜欢你,如果你希望赢得真正的友情,如果你希望在帮助别人的同时帮助自己,请牢记这条原则:真诚地关心别人。

2. 微笑的价值

喜欢微笑的人,在管理、教育和推销方面都比较成功,并且教育出来的孩子也更加开朗、乐观。

笑容比起皱眉更能传情达意,鼓励比惩罚更有效。

一个人脸上的神情,要远远比她身上所穿的衣服重要得多。

微笑会让人觉得你非常友善,会让你感受到一种温暖,它表示“我喜欢你,你令我非常开心,很高兴能见到你”。

微笑是你传播善意的信使,你的笑容可以照亮每一个看到它的人。对于那些看惯了皱眉、愁容满面,或是转过脸避开别人目光的人,你的笑容就如同阳光驱散雾霾般照进他们的心里。

3. 记住他人的姓名

姓名对于一个人来说是极为特殊、极为重要的。名字把人跟人区别开,使得一个人成为世上独一无二的个体。记得一个人的姓名,在人际交往上同样重要。

记住一个人的姓名,并轻易地叫出来,对这个人来说就是一种巧妙而有效的恭维。

4. 成为一名出色的听众

千万别忘记,那个与你交谈的人,他对于自己的需求和问题,比起对你以及你的问题的感兴趣程度要超过百倍。

认真地倾听别人谈话,是我们能给对方的一种最大的恭维。很少有人能够拒绝那种带有恭维性的认真聆听。

如果你想要成为一位出色的谈论家,一个善于倾听的人,想要别人对你感兴趣,那么你首先需要对他人感兴趣,提出一些别人愿意作答的问题,鼓励对方多谈论自己,以及他们的成就。

5. 让别人觉得自己重要

生活中,存在着一个举足轻重的原则,如果我们遵从这条原则,就永远也不会遇到麻烦;要是不遵从它,就会陷入无尽的麻烦。这个原则就是:让别人感受到他们自己的重要性。

人类本性中最深层的渴望,就是试图让自己变得重要。

主要让别人觉得自己重要,那么很多人的命运就很有可能因此而改变。

几乎每个你遇到的人都会认为他在某些地方比你强,这是一条永恒不变的真理。赢得这种人心的办法就是,以一种微妙的方式让他们意识到了你了解他们的重要性,并真诚地认同他们。

三人行,必有我师焉。要会向别人学习。

走出孤独忧虑的人生

1. 让自己忙起来

让自己不停地忙着。忧虑的人一定要让自己沉浸在工作中,否则只有在绝望中挣扎。

2. 不要为小事烦恼

不要让自己因为一些应该抛弃和忘记的小事而忧虑。要记住:生命如此短暂,不要再为小事而烦恼。

3. 不要自寻烦恼

要使我们能够停止忧虑,就可以根据事情发生的概率来评估我们的忧虑究竟值不值,这样,我想我们应该可以减少99%的忧虑。

让我们看看以前的记录,让我们根据概率问问自己,现在担心会发生的事情,到底真正发生的有多少?

4. 适应不可避免的事实

不要为月亮哭泣,也不要因事而后悔。

顺应时势,就是你在踏上人生旅途时最重要的一件事。

很显然,环境本身并不能使我们快乐或不快乐,只有我们对周围环境的反应才决定了我们的感受。

在必要的时候,我们都应该经受得住灾难和悲剧,甚至要战胜它们。也许我们会认为自己办不到,但事实上,我们内在的力量却强大得惊人,只要我们愿意利用,他就能帮助我们克服一切困难。

当我们不再反抗那些不可避免的事实之后,我们就可以节省精力,创造更丰富的生活。

5. 不要做无用功

在避免忧虑方面,没有比“船到桥头自然直”和“不要为打翻的牛奶哭泣”更基本,也更有用的话。

让过去的错误产生价值的唯一方法,就是平静地分析过去的错误,并从中吸取教训,然后再忘记错误。

不要试着去锯木屑。

不要为工作和金钱而烦恼

1. 做自己喜欢的工作

一定要记住,在做你生命中最重要而且影响最深远的决定之前,请务必多花点时间了解真实的真相。如果不这么做,那么你的决定将可能让你痛苦不已。

如果人们从事他们自己无限热爱的工作,他们都可以获得成功。

避免选择那些早就很激烈并且拥挤的职业和行业;避免选择只有10%的生存机会的行业。

克服“你只适合一项职业”的错误认知。每个人都可以在多项职业上取得成功。

2. 处理好金钱引发的烦恼

人们的大多数烦恼都和金钱有关。

使大多数人烦恼的并不是他们没有足够的金钱,而是他们不知道如何支配手中已有的金钱。

结构化并发

让我们先将思路转为日常业务开发中,比如在某某业务中,可能存在好几个需要同时处理的逻辑,比如同时请求两个网络接口,同时操作两个子任务等。我们暂且称上述学术化概念为 多个并发操作。

而每个并发操作其实都是在处理一个单独的任务,这个任务中,可能还存在子任务 ; 同样对于这个子任务来说,它又是其父任务的子单元。每个任务都有自己的生命周期,子任务的生命周期会继承父任务的生命周期,比如如果父任务关闭,子任务也会被取消。而如果满足这样特性,我们就称其就是 结构化并发。

异常传递流程

默认情况下,任意一个协程发生异常时都会影响到整个协程树,而异常的传递通常是双向的,也即协程会向子协程与父协程共同传递。

整体流程如下:

  1. 先 cancel 子协程
  2. 取消自己
  3. 将异常传递给父协程
  4. (重复上述过程,直到根协程关闭)

异常传递形式

  • launch : 层层向上传递,在launch外部try-catch无效

  • async : 根协程启动或加SupervisorJob启动,会优先当前暴露,可在await()捕获;否则会将异常传递给父协程,导致异常没有在调用处暴露。

异常处理

是否是根协程启动

async + 是否是根协程启动

  • async 时内部也是新的作用域,如果 async 对应的是根协程(scope.async),那么我们可以在 await() 时直接捕获异常
  • async 时如果不是根协程启动,则会将异常传递给父协程,导致异常没有在调用处暴露。可以在async时声明SupervisorJob,此时即可在 await() 时捕获异常

launch + 是否是根或父协程自带异常处理器

  • 在launch外部try-catch无用
  • launch自带的异常处理器不会处理当前协程发生的异常,除非当前协程显式添加SupervisorJob

SupervisorJob

根协程自带的SupervisorJob,子协程会继承

非根协程的父协程声明的SupervisorJob,子协程不会继承: 子协程在 launch 时会创建新的协程作用域,其会使用默认新的 Job 替代父协程传递 SupervisorJob ,

所以导致我们传递的 SupervisorJob 被覆盖。所以如果我们想让子协程不影响父协程或者其他子协程,此时就必须再显示添加 SupervisorJob。

场景实践

严格意义上来说,所有异常都可以用 tryCatch 去处理

严格意义上来说,所有异常都可以用 tryCatch 去处理,只要我们的处理位置得当。但这并不是所有方式的最优解,特别是如果你想更优雅的处理异常时,此时就可以考虑 CoroutineExceptionHandler 。

什么时候该用 SupervisorJob ,什么时候该用 Job?

引用官方的一句话就是:想要避免取消操作在异常发生时被传播,记得使用 SupervisorJob ;反之则使用 Job。

SupervisorJob + tryCatch

我们有两个接口 A,B 需要同时请求,当接口A异常时,需要不影响B接口的正常展示,当接口B异常时,此时界面展示异常信息

SupervisorJob + CoroutineExceptionHandler

如果你有一个顶级协程,并且需要自动捕获所有的异常,则此时可以选用上述方式

参考

普通callback实现

与协程异步代码同步写法对比

callback实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//CallbackImpl.kt‭​​‌‌‎‭‬​
abstract class CallbackImpl(val callBack: CallBack<Any>): CallBack<Any> {‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
override fun onResult(result: Any?) {‭​​‌‌‎‭‬​
val cb = callBack‭​​‌‌‎‭‬​
while (true) {‭​​‌‌‎‭‬​
val outcome = invoke(result)‭​​‌‌‎‭‬​
if (outcome === CALLBACK_FLAG) {‭​​‌‌‎‭‬​
return‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
cb.onResult(outcome)‭​​‌‌‎‭‬​
return‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
protected abstract fun invoke(result: Any?): Any?‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
val CALLBACK_FLAG = CoroutineSingletons.CALLBACK‭​​‌‌‎‭‬​
enum class CoroutineSingletons { CALLBACK }‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
interface CallBack<T> {‭​​‌‌‎‭‬​
fun onResult(result: T?)‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
object UserInfoHelper {‭​​‌‌‎‭‬​
fun launch() {‭​​‌‌‎‭‬​
Log.e("MainActivity", "launch run")‭​​‌‌‎‭‬​
requestUserCallback(object : CallBack<Any> {‭​​‌‌‎‭‬​
override fun onResult(result: Any?) {‭​​‌‌‎‭‬​
Log.e("MainActivity", "launch result: $result")‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
})‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
val handler = Handler(Looper.getMainLooper())‭​​‌‌‎‭‬​
private fun delay(timeMillis: Long, callBack: CallBack<Any>): Any {‭​​‌‌‎‭‬​
handler.postDelayed({‭​​‌‌‎‭‬​
callBack.onResult(Unit)‭​​‌‌‎‭‬​
}, timeMillis)‭​​‌‌‎‭‬​
return CALLBACK_FLAG‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
//正常情况下应该保持到协程的context中,这样写为了简单处理‭​​‌‌‎‭‬​
private var userCallback:UserCallback? = null‭​​‌‌‎‭‬​
abstract class UserCallback(callBack: CallBack<Any>) : CallbackImpl(callBack) {‭​​‌‌‎‭‬​
var result: Any? = null‭​​‌‌‎‭‬​
var flag: Int = 0‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
fun requestUserCallback(callBack: CallBack<Any>): Any {‭​​‌‌‎‭‬​
val cb = if (userCallback == null) {‭​​‌‌‎‭‬​
val c = object : UserCallback(callBack) {‭​​‌‌‎‭‬​
override fun invoke(result: Any?): Any? {‭​​‌‌‎‭‬​
this.result = result‭​​‌‌‎‭‬​
++flag‭​​‌‌‎‭‬​
return requestUserCallback(this)‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
userCallback = c‭​​‌‌‎‭‬​
c‭​​‌‌‎‭‬​
}else {‭​​‌‌‎‭‬​
userCallback!!‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
var returnResult = cb.result‭​​‌‌‎‭‬​
when (cb.flag) {‭​​‌‌‎‭‬​
0 -> {‭​​‌‌‎‭‬​
if (requestFriendListCallback(cb) == CALLBACK_FLAG) {‭​​‌‌‎‭‬​
return CALLBACK_FLAG‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
1 -> {‭​​‌‌‎‭‬​
returnResult = cb.result‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
return "user: $returnResult"‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
//正常情况下应该保持到协程的context中,这样写为了简单处理‭​​‌‌‎‭‬​
var requestFriendListCallback: RequestFriendListCallback? = null‭​​‌‌‎‭‬​
abstract class RequestFriendListCallback(callBack: CallBack<Any>) : CallbackImpl(callBack) {‭​​‌‌‎‭‬​
var result: Any? = null‭​​‌‌‎‭‬​
var flag: Int = 0‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
fun requestFriendListCallback(callBack: CallBack<Any>): Any {‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
val cb = if ( requestFriendListCallback == null) {‭​​‌‌‎‭‬​
val c = object : RequestFriendListCallback(callBack) {‭​​‌‌‎‭‬​
override fun invoke(result: Any?): Any? {‭​​‌‌‎‭‬​
this.result = result‭​​‌‌‎‭‬​
++flag‭​​‌‌‎‭‬​
return requestFriendListCallback(this)‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
requestFriendListCallback = c‭​​‌‌‎‭‬​
c‭​​‌‌‎‭‬​
} else {‭​​‌‌‎‭‬​
requestFriendListCallback!!‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
when (cb.flag) {‭​​‌‌‎‭‬​
0 -> {‭​​‌‌‎‭‬​
if (delay(3000, cb) == CALLBACK_FLAG) {‭​​‌‌‎‭‬​
return CALLBACK_FLAG‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
1 -> {‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
 ‭​​‌‌‎‭‬​
return "friendList"‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​
}‭​​‌‌‎‭‬​

协程反编译字节码

1
2


1. suspendCoroutine作用及用法

suspendCoroutine 是 Kotlin 中用于挂起协程并与外部世界进行交互的函数之一。它的主要作用是允许你在协程中将异步非挂起的操作转化为挂起操作,从而可以方便地与协程上下文集成。具体来说,suspendCoroutine 的主要作用包括以下几点:

  1. 将回调式的异步操作转化为挂起操作:当你需要在协程中执行某些异步操作,例如调用回调函数,处理回调式 API,或等待某个事件发生时,suspendCoroutine 允许你将这些异步操作封装为挂起操作,使得你可以像调用普通的挂起函数一样调用它们,从而简化了异步编程的代码。
  2. 挂起当前协程:suspendCoroutine 在执行时会挂起当前协程,这意味着协程的执行会暂停,直到回调操作完成。这有助于防止阻塞线程,并允许其他协程在执行。
  3. 传递回调函数:suspendCoroutine 接受一个 Lambda 表达式,该 Lambda 表达式需要传递一个函数参数,通常是一个回调函数,该回调函数将在异步操作完成时被调用。在 Lambda 表达式内部,你可以使用 resume 来恢复协程的执行并传递结果,或者使用 resumeWithException 来传递异常,以便协程可以处理结果或错误。

suspendCoroutine案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
suspend fun main() {  
withContext(Dispatchers.IO) {
val result = async { getResult() }.await()
println("result=$result")
}
}

suspend fun getResult() = suspendCoroutine<Boolean> {
val callback = object : Callback {
override fun onSuccess() {
it.resume(true)
}

override fun onFail() {
it.resumeWithException(Exception("fail"))
}

override fun onCancel() {
it.resume(false)
}
}
handleCallback(callback)
}

fun handleCallback(callback: Callback) {
callback.onFail()
}

interface Callback {
fun onSuccess()
fun onFail()
fun onCancel()
}

suspendCancellableCoroutine作用和用法

suspendCancellableCoroutine 是 Kotlin 中的一个函数,它允许你在一个协程中创建一个可取消的挂起操作,通常用于实现自定义的挂起函数或处理异步操作。基本功能和suspendCoroutine一样,只是多了一个支持监听外部协程取消和取消内部协程执行的能力。

suspendCancellableCoroutine案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 自定义的可取消挂起函数  
suspend fun customCancelableOperation(): String {
return suspendCancellableCoroutine { continuation ->
val job = GlobalScope.launch {
try {
// 模拟异步操作
delay(1000)
// 如果操作成功,恢复挂起操作并传递结果
continuation.resume("Operation completed successfully")
} catch (e: CancellationException) {
// 如果操作被取消,处理取消操作
continuation.resumeWithException(e)
} catch (e: Exception) {
// 如果操作失败,传递异常
continuation.resumeWithException(e)
}
}

// 当外部协程取消时,取消内部协程
continuation.invokeOnCancellation {
job.cancel()
}
}
}

fun main() = runBlocking {
val job = launch {
val result = customCancelableOperation()
println(result)
}

delay(500) // 等待一段时间
job.cancel() // 取消外部协程
job.join()
}

处理resumeWithException的通用方案

除了通过try catch的方式来捕获异常,也可以在launch协程的context中添加exceptionHandler来统一处理异常。 例如:

1
2
3
4
5
6
7
8
9
10
11
fun main() = runBlocking {  
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: $exception")
}

val job = GlobalScope.launch(exceptionHandler) {
println(data)
}

job.join()
}

其他

在日常开发中,在协程上下文将异步非挂起的操作转化为挂起操作时遇到过多次回调完成的情况,导致一个continuation被多次resume,从而出现异常java.lang.IllegalStateException: Already resumed...

这时,可以通过使用suspendCancellableCoroutine并通过判断其状态决定是否需要真正resume来避免重复多次resume。

1
2
3
4
5
6
7
8
9
fun <T> Continuation<T>.safeResume(value: T) {  
if (this is CancellableContinuation) {
if (isActive) {
resume(value)
} else {
CapaLog.d("Continuation", "continuation is not active")
}
} else throw Exception("Must use suspendCancellableCoroutine instead of suspendCoroutine")
}

本文记录自定义ViewGroup实践中提炼出来的经验。

ViewGroup是啥?

ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity),当然还有margin等;于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ;决定childView的位置;为什么只是建议的宽和高,而不是直接确定呢,别忘了childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。

2、View的职责是啥?
View的职责,根据测量模式和ViewGroup给出的建议的宽和高,计算出自己的宽和高;同时还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形态。

3、ViewGroup和LayoutParams之间的关系?
大家可以回忆一下,当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义了LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。

2、View的3种测量模式
上面提到了ViewGroup会为childView指定测量模式,下面简单介绍下三种测量模式:

EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY;

AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;

UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。

注:上面的每一行都有一个一般,意思上述不是绝对的,对于childView的mode的设置还会和ViewGroup的测量mode有一定的关系;当然了,这是第一篇自定义ViewGroup,而且绝大部分情况都是上面的规则,所以为了通俗易懂,暂不深入讨论其他内容。

3、从API角度进行浅析
上面叙述了ViewGroup和View的职责,下面从API角度进行浅析。

View的根据ViewGroup传人的测量值和模式,对自己宽高进行确定(onMeasure中完成),然后在onDraw中完成对自己的绘制。

ViewGroup需要给View传入view的测量值和模式(onMeasure中完成),而且对于此ViewGroup的父布局,自己也需要在onMeasure中完成对自己宽和高的确定。此外,需要在onLayout中完成对其childView的位置的指定。