-
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
这段程序在模拟计算文件的 Md5 值。我们知道,文件的 Md5 值计算是一项耗时操作,所以我们希望启动一个协程来处理这个耗时任务,并在任务运行结束时打印出来计算的结果。
我们先来一段一段分析下这个示例:
/** * 上下文,用来存放我们需要的信息,可以灵活的自定义 */ class FilePath(val path: String): AbstractCoroutineContextElement(FilePath){ companion object Key : CoroutineContext.Key<FilePath> }
-
1
-
2
-
3
-
4
-
5
-
6
我们在计算过程中需要知道计算哪个文件的 Md5,所以我们需要通过上下文把这个路径传入协程当中。如果有多个数据,也可以一并添加进去,在运行当中,我们可以通过 Continuation 的实例拿到上下文,进而获取到这个路径:
continuation.context[FilePath]!!.path
- 1
接着,我们再来看下 Continuation:
val continuation = object : Continuation<Unit> { override val context: CoroutineContext get() = FilePath(path) override fun resume(value: Unit) { log("resume: $value") } override fun resumeWithException(exception: Throwable) { log(exception.toString()) } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
我们除了给定了 FilePath 这样一个上下文之外就是简单的打了几行日志,比较简单。这里传入的 Continuation 当中的 resume 和 resumeWithException 只有在协程最终执行完成后才会被调用,这一点需要注意一下,也正是因为如此,startCoroutine 把它叫做 completion:
public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>
- 1
那么下面我们看下最关键的这段代码:
asyncCalcMd5("test.zip") { log("in coroutine. Before suspend.") //暂停我们的协程,并开始执行一段耗时操作 val result: String = suspendCoroutine { continuation -> log("in suspend block.") continuation.resume(calcMd5(continuation.context[FilePath]!!.path)) log("after resume.") } log("in coroutine. After suspend. result = $result") }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
suspendCoroutine 这个方法将外部的代码执行权拿走,并转入传入的 Lambda 表达式中,而这个表达式当中的操作就对应异步的耗时操作了,在这里我们“计算”出了 Md5 值,接着调用 continuation.resume
将结果传了出去,传给了谁呢?传给了 suspendCoroutine 的返回值也即 result,这时候协程继续执行,打印 result 结束。
下面就是运行结果了:
2017-01-30T06:43:52.284Z [main] before coroutine 2017-01-30T06:43:52.422Z [main] in coroutine. Before suspend. 2017-01-30T06:43:52.423Z [main] in suspend block. 2017-01-30T06:43:52.423Z [main] calc md5 for test.zip. 2017-01-30T06:43:53.426Z [main] after resume. 2017-01-30T06:43:53.427Z [main] in coroutine. After suspend. result = 1485758633426 2017-01-30T06:43:53.427Z [main] resume: 1485758633426 2017-01-30T06:43:53.427Z [main] after coroutine
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
细心的读者肯定一看就发现,所谓的异步操作是怎么个异步法?从日志上面看,明明上面这段代码就是顺序执行的嘛,不然 after coroutine 这句日志为什么非要等到最后才打印?
还有,整个程序都只运行在了主线程上,我们的日志足以说明这一点了,根本没有异步嘛。难道说协程就是一个大骗子??
3. 实现异步
这一部分我们就要回答上一节留下的问题。不过在此之前,我们再来回顾一下协程存在的意义:让异步代码看上去像同步代码,直接自然易懂。至于它如何做到这一点,可能各家的语言实现各有不同,但协程给人的感觉更像是底层并发 API(比如线程)的语法糖。当然,如果你愿意,我们通常所谓的线程也可以被称作操作系统级 API 的语法糖了吧,毕竟各家语言对于线程的实现也各有不同,这个就不是我们今天要讨论的内容了。
不管怎么样,你只需要知道,协程的异步需要依赖比它更底层的 API 支持,那么在 Kotlin 当中,这个所谓的底层 API 就非线程莫属了。
知道了这一点,我们就要考虑想办法来把前面的示例完善一下了。
首先我们实例化一个线程池:
private val executor = Executors.newSingleThreadScheduledExecutor { Thread(it, "scheduler") }
-
1
-
2
-
3
接着我们把计算 Md5 的部分交给线程池去运行:
asyncCalcMd5("test.zip") { log("in coroutine. Before suspend.") //暂停我们的线程,并开始执行一段耗时操作 val result: String = suspendCoroutine { continuation -> log("in suspend block.") executor.submit { continuation.resume(calcMd5(continuation.context[FilePath]!!.path)) log("after resume.") } } log("in coroutine. After suspend. result = $result") executor.shutdown() }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
那么结果呢?
2017-01-30T07:18:04.496Z [main] before coroutine 2017-01-30T07:18:04.754Z [main] in coroutine. Before suspend. 2017-01-30T07:18:04.757Z [main] in suspend block. 2017-01-30T07:18:04.765Z [main] after coroutine 2017-01-30T07:18:04.765Z [scheduler] calc md5 for test.zip. 2017-01-30T07:18:05.769Z [scheduler] in coroutine. After suspend. result = 1485760685768 2017-01-30T07:18:05.769Z [scheduler] resume: 1485760685768 2017-01-30T07:18:05.769Z [scheduler] after resume.
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
我们看到在协程被暂停的那一刻,协程外面的代码被执行了。一段时间之后,协程被继续执行,打印结果。
截止到现在,我们用协程来实现异步操作的功能已经实现。
你可能要问,如果我们想要完成异步操作,直接用线程池加回调岂不更直接简单,为什么要用协程呢,搞得代码这么让人费解不说,也没有变的很简单啊。
说的对,如果我们实际当中把协程的代码都写成这样,肯定会被蛋疼死,我前面展示给大家的,是 Kotlin 标准库当中最为基础的 API,看起来非常的原始也是理所应当的,如果我们对其加以封装,那效果肯定大不一样。
除此之外,在高并发的场景下,多个协程可以共享一个或者多个线程,性能可能会要好一些。举个简单的例子,一台服务器有 1k 用户与之连接,如果我们采用类似于 Tomcat 的实现方式,一个用户开一个线程去处理请求,那么我们将要开 1k 个线程,这算是个不小的数目了;而我们如果使用协程,为每一个用户创建一个协程,考虑到同一时刻并不是所有用户都需要数据传输,因此我们并不需要同时处理所有用户的请求,那么这时候可能只需要几个专门的 IO 线程和少数来承载用户请求对应的协程的线程,只有当用户有数据传输事件到来的时候才去相应,其他时间直接挂起,这种事件驱动的服务器显然对资源的消耗要小得多。
4. 进一步封装
这一节的内容较多的参考了 Kotlin 官方的
4.1 异步
刚才那个示例让我们感觉到,写个协程调用异步代码实在太原始了,所以我们决定对它做一下封装。如果我们能在调用 suspendCoroutine 的时候直接把后面的代码拦截,并切到线程池当中执行,那么我们就不用每次自己搞一个线程池来做这事儿了,嗯,让我们研究下有什么办法可以做到这一点。
拦截…怎么拦截呢?
public interface ContinuationInterceptor : CoroutineContext.Element { companion object Key : CoroutineContext.Key<ContinuationInterceptor> public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> }
-
1
-
2
-
3
-
4
-
5
我们发现,Kotlin 的协程 API 当中提供了这么一个拦截器,可以把协程的操作拦截,传入的是原始的 Continuation,返回的是我们经过线程切换的 Continuation,这样就可以实现我们的目的了。
open class Pool(val pool: ForkJoinPool) : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { override fun <T> interceptContinuation(continuation: Continuation<T>) : Continuation<T> = PoolContinuation(pool, //下面这段代码是要查找其他拦截器,并保证能调用它们的拦截方法 continuation.context.fold(continuation, { cont, element -> if (element != this@Pool && element is ContinuationInterceptor) element.interceptContinuation(cont) else cont })) } private class PoolContinuation<T>( val pool: ForkJoinPool, val continuation: Continuation<T> ) : Continuation<T> by continuation { override fun resume(value: T) { if (isPoolThread()) continuation.resume(value) else pool.execute { continuation.resume(value) } } override fun resumeWithException(exception: Throwable) { if (isPoolThread()) continuation.resumeWithException(exception) else pool.execute { continuation.resumeWithException(exception) } } fun isPoolThread(): Boolean = (Thread.currentThread() as? ForkJoinWorkerThread)?.pool == pool }
-
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
这个 Pool 是什么鬼?我们让它继承 AbstractCoroutineContextElement 表明它其实就是我们需要的上下文。实际上这个上下文可以给任意协程使用,于是我们再定义一个 object:
object CommonPool : Pool(ForkJoinPool.commonPool())
- 1
有了这个,我们就可以把没加线程池的版本改改了:
fun main(args: Array<String>) { log("before coroutine") //启动我们的协程 asyncCalcMd5("test.zip") { ... } log("after coroutine") //加这句的原因是防止程序在协程运行完之前停止 CommonPool.pool.awaitTermination(10000, TimeUnit.MILLISECONDS) } ... fun asyncCalcMd5(path: String, block: suspend () -> String) { val continuation = object : Continuation<String> { override val context: CoroutineContext //注意这个写法,上下文可以通过 + 来组合使用 get() = FilePath(path) + CommonPool ... } block.startCoroutine(continuation) } ...
-
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
那么运行结果呢?
2017-01-30T09:13:11.183Z [main] before coroutine 2017-01-30T09:13:11.334Z [main] after coroutine 2017-01-30T09:13:11.335Z [ForkJoinPool.commonPool-worker-1] in coroutine. Before suspend. 2017-01-30T09:13:11.337Z [ForkJoinPool.commonPool-worker-1] in suspend block. 2017-01-30T09:13:11.337Z [ForkJoinPool.commonPool-worker-1] calc md5 for test.zip. 2017-01-30T09:13:12.340Z [ForkJoinPool.commonPool-worker-1] after resume. 2017-01-30T09:13:12.341Z [ForkJoinPool.commonPool-worker-1] in coroutine. After suspend. result = 1485767592340 2017-01-30T09:13:12.341Z [ForkJoinPool.commonPool-worker-1] resume: 1485767592340
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
我们看到程序已经非常完美的实现异步调用。显然,这种写法要比线程池回调的写法看上去顺理成章得多。
4.2 启动协程
在讨论完异步的封装后,有人肯定还是会提出新问题:启动协程的写法是不是有点儿啰嗦了啊?没错,每次构造一个 Continuation,也没干多少事儿,实在没什么必要,干脆封装一个通用的版本得了:
class StandaloneCoroutine(override val context: CoroutineContext): Continuation<Unit> { override fun resume(value: Unit) {} override fun resumeWithException(exception: Throwable) { //处理异常 val currentThread = Thread.currentThread() currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception) } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
这样就好办了,我们每次启动协程只需要针对当前协程提供特定的上下文即可,那么我们是不是再把启动的那个函数改改呢?
fun launch(context: CoroutineContext, block: suspend () -> Unit) = block.startCoroutine(StandaloneCoroutine(context))
-
1
-
2
有了这个,我们前面的代码就可以进一步修改:
fun main(args: Array<String>) { log("before coroutine") //启动我们的协程 launch(FilePath("test.zip") + CommonPool) { log("in coroutine. Before suspend.") //暂停我们的线程,并开始执行一段耗时操作 val result: String = suspendCoroutine { continuation -> log("in suspend block.") continuation.resume(calcMd5(continuation.context[FilePath]!!.path)) log("after resume.") } log("in coroutine. After suspend. result = $result") } log("after coroutine") CommonPool.pool.awaitTermination(10000, TimeUnit.MILLISECONDS) } /** * 上下文,用来存放我们需要的信息,可以灵活的自定义 */ class FilePath(val path: String) : AbstractCoroutineContextElement(Key) { companion object Key : CoroutineContext.Key<FilePath> } fun calcMd5(path: String): String { log("calc md5 for $path.") //暂时用这个模拟耗时 Thread.sleep(1000) //假设这就是我们计算得到的 MD5 值 return System.currentTimeMillis().toString() }
-
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
运行结果自然也没什么好说的。
4.3 暂停协程
暂停协程这块儿也太乱了,看着莫名其妙的,能不能直白一点儿呢?其实我们的代码不过是想要获取 Md5 的值,所以如果能写成下面这样就好了:
val result = calcMd5(continuation.context[FilePath]!!.path).await()
- 1
毋庸置疑,这肯定是可以的。想一下,有哪个类可以支持我们直接阻塞线程,等到获取到结果之后再返回呢?当然是 Future 了。
suspend fun <T> CompletableFuture<T>.await(): T { return suspendCoroutine { continuation -> whenComplete { result, e -> if (e == null) continuation.resume(result) else continuation.resumeWithException(e) } } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/730cba0431ac14862861b8e27e038e48.jpeg)
最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。
Android高级架构师之路很漫长,一起共勉吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-8uIUzSCP-1712468913811)]
[外链图片转存中…(img-t87ioxOY-1712468913812)]
[外链图片转存中…(img-vwoGGq9B-1712468913813)]
[外链图片转存中…(img-vs89ev2G-1712468913813)]
[外链图片转存中…(img-W5Y87hFM-1712468913814)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/730cba0431ac14862861b8e27e038e48.jpeg)
最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。
[外链图片转存中…(img-hUhjkaLi-1712468913814)]
Android高级架构师之路很漫长,一起共勉吧!