Kotlin 协程学习,2024android面试题

2. 开启协程的三个基础方法

================================================================================

在Kotlin本来的包中时没有协程api的,所以要在build.gradle中引入:

// build.gradle

implementation “org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version”

implementation “org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version”

协程的入口是 launch{} 语句块,但它不是顶层函数,所以不能直接使用,有三种实现,分别来看看他们来实现Hello World

2.1 使用GlobalScope.launch实现


使用 GlobalScope.launch 可以手动开启一个协程:

fun main() {

println(“Thread name:” + Thread.currentThread().name)

GlobalScope.launch { // 在后台启动一个新的协程并继续

println(“Coroutines Thread name:” + Thread.currentThread().name)

delay(100L) // 假设这是一个100ms耗时任务

println(“World!”) // 这是耗时任务的结果

}

println(“Hello,”) // 主线程中的代码会立即执行

Thread.sleep(200L) // 延迟 200 毫秒来保证 JVM 的存活

}

// 打印:

Thread name:main

Hello,

Coroutines Thread name:DefaultDispatcher-worker-1

World!

在开启的协程中,我们使用 delay来让协程挂起100ms后答应world,表明这是一个耗时的任务。

主线程中输入了Hello后,使用Thread.sleep()来阻塞主线程,防止在协程的任务还没执行完时,主线程就关闭了。

这个时候我们会发现,协程所在的线程和外边线程不一致。

2.2 使用runBlocking实现


runBlocking{..}代码块里面,就是协程。相比于GlobalScope,它是阻塞线程的,一般开发中我们用不到,常用于单元测试,来看一下使用语句块实现:

fun main() {

println(“Thread name:” + Thread.currentThread().name)

runBlocking {

println(“Coroutines Thread name:” + Thread.currentThread().name)

delay(1000L)

print("Hello, ")

}

print(“World!”)

}

// 打印结果:

Thread name:main

Coroutines Thread name:main

Hello, World

可以看到 runBlocking{...}开启了一个协程,但是他没有在新线程里面执行,而是一直阻塞到里面的代码块完成。

可以来看下下面的用法:

fun main() = runBlocking { // 使用runBlocking开启一个协程

launch { // 里面再开启一个协程

delay(1000L)

println("World! " + Thread.currentThread().name)

}

println("Hello, " + Thread.currentThread().name)

}

// 打印结果:

Hello, main

World! main

这个代码中,我们在 runBlocking{}里,又开启了一个launch协程,相当于大协程里面的小协程。

我们没有使用 Thread.sleep()来防止主线程执行完而launch中的代码还没有执行的问题,它依然能够等所有任务都执行完后才关闭程序。

上面两片代码可以得出runBlocking的两个结论

  1. runBlocking{}协程会产生线程阻塞,每当线程执行到这个代码块时,会等到代码块里的所有内容执行完,才会继续走下面的代码

  2. runBlocking{}协程里面可以加入其它的小协程。这个时候,可以把这个代码块看成一个线程的执行顺序(但本身不是线程)

2.3 使用CoroutineScope实现


在讲解 CoroutineScope之前,我们需要先搞清楚: CountineScopecountineScope{...}是两个东西

  • 前者可以创建一个协程作用域

  • 后者产生的是一个挂起函数

他们虽然名称相同,但是作用有很大的不同,我认为官方文档举的例子不是很好,让我饶了很大的弯路。有兴趣的可以读一下这篇:[Coroutines: runBlocking vs coroutineScope

]( )

fun main(){

val coroutineScope = CoroutineScope(Dispatchers.Unconfined) // 1 创建一个CoroutineScope对象

coroutineScope.launch { // 使用该对象开启一个协程

delay(100L) // 模拟一个100ms的延时任务

println(“World”)

}

print("Hello, ")

Thread.sleep(200L) // 让主线程睡200MS以免主线程关闭

}

// 打印结果:

Hello, World

可以看到它和一开始的 GlobalScope.launch{..}的作用差不多,但它们是有许多差别的,在Android并不推荐使用 GlobalScope.launch{..},而是推荐使用 CoroutineScope{..},原因后面会讲到。

注释1中, CoroutineScope的构造函数里面传入的是 Dispatchers.Unconfined(不受限的工作在主线程中),它是一个 CoroutineContext类,它的作用是调度这个协程在哪个线程工作,和Android的Context不一样,叫这个名字是帮助我们去理解。所以这个Context被称为 协程上下文

因为平时经常会遇到线程切换的场景,我们可以利用这个协程上下文来实现线程的切换,在上面的代码中,模拟了一个我们做耗时任务的操作,那么接下来有个场景,我想在后台线程去做这个任务,做完之后把结果拿回主线程进行输出,那我们可能会这么写:

coroutineScope.launch(Dispatchers.IO) {

… // 做耗时任务

launch(Dispatchers.Unconfined) {

… // 去到想要的线程做xxx

}

}

这个时候就发现产生了嵌套!?

确实,如果只是使用launch{...},它本身的能力是有限的, 但是协程中却提供了一个很有帮助的函数:withContext(),这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行,看下下面的一个代码:

fun main() {

val coroutineScope = CoroutineScope(Dispatchers.Unconfined)

coroutineScope.launch(Dispatchers.Unconfined) { // 在未指定线程中开启协程

val text = withContext(Dispatchers.IO) { // 开启一个子协程,并指定在I/O线程,去做一个耗时操作

delay(200L) // 1 做一个200MS的耗时操作

“World”

}

print(text)

}

print("Hello, ")

Thread.sleep(300L) // 让主线程睡300MS,防止主线程关闭

}

// 打印结果:

Hello, World

如果说 withContext(){..}里面的代码很长,或者需要复用,我们需要将他们抽成一个方法,可以写成这样:

coroutineScope.launch(Dispatchers.Unconfined) {

val text = withContext(Dispatchers.IO) {

getText()

}

}

suspend fun getText(): String {

delay(200L)

return “World”

}

我们抽出了一个方法,并在前面加上了 suspend 关键字。

这个关键字是Kotlin协程中最为核心鹅关键字,它的中文意思是 [暂停] 或者 [挂起],它所代表的意义其实就是将当前协程任务在线程中挂起: 线程在执行到遇到挂起的代码时,可以绕过去,不用等到这些代码执行完毕才能执行后面的代码,而是你走你的,我走我的,你占的道反正不在我这。

launch{..}里面的代码块,其实就是默认被 suspend修饰了,所以抽出来的方法,我们也额外的手动加上。

3. 非阻塞式挂起

===========================================================================

3.1 挂起


挂起是什么?请看下面的代码(还是上面那个例子):

coroutineScope.launch(Dispatchers.Unconfined) {

val text = withContext(Dispatchers.IO) {

getText()

}

print(text)

}

上面可以看成做了两个事情:

  • 开启一个(Unconfined即当前线程所在的)协程

  • 在这个协程中 开启一个(IO线程)子协程做耗时任务

这个时候,我们就成 coroutineScope.launch{..}挂起了。

我们以线程的角度来考虑这段代码,请看下图:

在这里插入图片描述

假设在CPU单核运算,只有一条线来走时间片轮换,那我们假设每个时间只有一个线程在执行代码。

那么在走到 withContext(Dispatchers.IO){..}时,它其实就是开启了一个IO线程。并将代码块中的代码放入到这个线程里面去做。

那这不就是切线程吗?为啥美其名曰挂起?

是因为,这个协程的API在完成了I/O线程的任务之后,不需要我们手动切回到原来的线程(可以对比一下RxJava在子线程做完网络请求后我们手动切回主线程),自动的帮我们切回了原来的线程。

我上面的代码,在 withContext(){..} I/O线程做完延时操作后,自然而然的回到了 Unconfined的线程去做print。

这个恢复到原来线程的操作在Kotlin里叫 resume。它本身一点儿也不稀奇,因为我们在RxJava中就已经知道通过Handler来切换线程了, 只是Kotlin包装到我们意识不到原来已经切换回来的程度。

总结成一句话:挂起就是: 切换线程 + 恢复到原来线程

3.2 suspend


我们用一个 suspend 来修饰挂起函数。suspend的意义是什么?它能实现挂起吗?

答案是否定的,单单给一个普通函数加上一个 suspend,没有任何效果,可以看下下面代码:

在这里插入图片描述

我们在函数中调用了 print(),发现 suspend置灰了,这说明只是用 suspend是无法让函数挂起的。

CoroutineScope.launch(){}代码块能让语句挂起吗?答案也是否定的。

那让函数挂起的语句是什么?

答案是 withContext(){..},它是真正的创建了线程的代码,所以当出现了这个语句,协程才能真正的挂起。

当然,除了 withContext() ,还有别的能让协程挂起,比如 delay(),因为他本身也是个操作线程的语句,包括别的。

总结下suspend的作用:

  • 提醒调用者,该函数会切换线程(也就是挂起)

  • 静态代码扫描,检测该函数是否有线程操作

  • 只有在协程中调用withContext(){..}类的代码, 该协程才会被挂起

3.3 非阻塞式挂起的含义


当我们了解了挂起的本质是切换线程,那我们就明白了非阻塞的含义是什么了。

在Java开发中,我们通过调用 Executor来开一个子线程,子线程会阻塞主线程的代码吗?

答案是不会的,因为当我们只是开一个子线程出来,主线程还是会继续走,等时间片不在主线程时,子线程再去走,但对于主线程来说,它里面的代码并没有因为子线程的存在而产生阻塞。

↑所以这个情况,也是非阻塞的。

而Koltin中的【非阻塞式挂起】,其实和Java一样,它只是通过协程达到了这个效果:代码明明看起来是阻塞的,但是运行却是不阻塞的。 如下所示:

coroutineScope.launch(Dispatchers.Main) {

val avatar = async { api.getAvatar(user) } // 获取用户头像

val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo

val merged = suspendingMerge(avatar, logo) // 合并结果

show(merged) // 更新 UI

}

再往前想一想 runBlocking(){..}为啥线程阻塞了?因为它本身就没有切换线程,它是直接在本线程上做耗时任务。

嘿嘿,是不是有点恍然大悟了?它本身就是噱头。

4. 协程的取消和超时

=============================================================================

官方的例子举得很好,接下来就都用官方的代码了~

4.1 协程的取消


4.1.1 协程的取消

协程任务是可以取消的

// 省略一些CoroutineScope的代码

val job : 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.”)

// 执行结果:

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.

我们可以看到 CoroutineScope 提供了一个 Job类型的句柄,我们可以调用其 Job.cancel()Job.join()来停止和加入该协程。

这两个方法也可以合并成一个 Job.cancelAndJoin()来使用

4.1.2 协程是不能随时取消的

和线程一样,我们在调用了 Thread.cancel()后,线程不会马上取消。

协程也一样,当协程在进行计算任务任务时,如果你直接cancel,是不会取消任务的:

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.”)

// 打印结果:

job: I’m sleeping 0 …

job: I’m sleeping 1 …

job: I’m sleeping 2 …

main: I’m tired of waiting!

job: I’m sleeping 3 …

job: I’m sleeping 4 …

main: Now I can quit.

我们需要像 Thread那样,去调用isInterrupted()来检查。

协程提供了一个变量 isActive来判断当前协程是否取消了,如果取消了,就不用再走下去了:

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.”)

// 打印结果:

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.

4.1.3 在取消时释放内存

我们是可以感知得到Job被取消的,当Job被取消的时候,它会抛出一个 CancellationException,我们把它当成结束回调,在里面做释放资源:

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.”)

4.1.4 假如你的释放资源也是耗时的怎么办

这是一个比较少见的情况,在 finally块中调用了耗时操作,此时协程又被取消,那相当于你finally的耗时操作执行不了

协程提供了 withContext(NonCancellable){...}来帮助协程延迟取消:

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.”)

4.2 协程的超时


在实践中绝大多数取消一个协程的理由是它有可能超时。

当你手动追踪一个相关 Job 的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout() 函数来做这件事。

来看看示例代码:

withTimeout(1300L) {

repeat(1000) { i ->

println(“I’m sleeping $i …”)

delay(500L)

}

}

// 打印结果:

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

发现抛出了 TimeoutCancellationException异常,它是 CancellationException的子类。

之所以后者没有产生堆栈信息,是因为协程认为我们取消它,这是一个正常的结束。

如果遇到特殊的场景,比如我必须要知道这个任务在规定时间内还没有做完,,可以使用类似 withTimeoutwithTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:

val result = withTimeoutOrNull(1300L) {

repeat(1000) { i ->

println(“I’m sleeping $i …”)

delay(500L)

}

“Done” // 在它运行得到结果之前取消它

}

println(“Result is $result”)

// 打印结果:

I’m sleeping 0 …

I’m sleeping 1 …

I’m sleeping 2 …

Result is null

就不会抛出异常,而是返回一个null。个人认为这样的做法比catch异常要温柔一点。

5. coroutineScope

===================================================================================

coroutineScope{..} 会创建一个挂起函数,所以调用它的函数必须被 suspend修饰。

官方文档是这么描述的:

This function is designed for parallel decomposition of work.

When any child coroutine in this scope fails, this scope fails and all the rest of the children are cancelled

This function returns as soon as the given block and all its children coroutines are completed.

翻译一下就是

这个方法是为了并行工作而设计的

当这个作用域内的任何一个子协程失败,那么这个作用域失败,并且其所有剩余的子协程都失败

当这个作用域所有的子协程都成功完成任务了,那么这个作用域将会返回(结果)

5.1 coroutineScope 和 CoroutineScope的区别


因为 coroutineScope的描述,所以可以从结构化和非结构化入手,coroutineScope是结构化的,CoroutineScope是非结构化的。

结构化更利于并行程序的执行。

举个例子,我们在主线程中,点击一个Button,这个button会去做耗时操作,然后返回结果,使用 CoroutineScope是这样的:

btn.setOnClickListener( () -> {

CoroutineScope(Dispatchers.Main).launch {

int result = downloadUserData()

Toast.makeText(applicationContext, "Result : " + result, Toast.LENGTH_LONG).show()

});

private suspend int downloadUserData() {

int result = 0;

// 这里我们使用一个CoroutineScope并切换线程

CoroutineScope(Dispatchers.IO).launch {

for (int i = 0; i < 2000; i++) {

kotlinx.coroutines.delay(400);

result++;

}

}

return result;

}

}

Toast打印出来的result是0

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后,如果大伙有什么好的学习方法或建议欢迎大家在评论中积极留言哈,希望大家能够共同学习、共同努力、共同进步。

小编在这里祝小伙伴们在未来的日子里都可以 升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰!!

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言,一定会认真查询,修正不足,谢谢。

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

…(img-7uFPVgRe-1712197676222)]
[外链图片转存中…(img-9Sx1hoWE-1712197676222)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-Unyzo1bD-1712197676222)]

最后,如果大伙有什么好的学习方法或建议欢迎大家在评论中积极留言哈,希望大家能够共同学习、共同努力、共同进步。

小编在这里祝小伙伴们在未来的日子里都可以 升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰!!

不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言,一定会认真查询,修正不足,谢谢。

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

  • 11
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值