Kotlin 的协程用力瞥一眼,android基础入门

以下内容来自文章作者 LewisLuo

码上开学 Kotlin 系列的文章,协程已经是第五期了,这里简单讲一下我们(扔物线和即刻 Android 团队)出品的 Kotlin 上手指南系列文章的一些考量:

  • 官方文档有指定的格式,因为它是官方的,必须面面俱到,写作顺序不是由浅入深,不管你懂不懂,它都得讲。
  • 网上的文章大都是从作者自身的角度出发,真正从读者的需求出发的少之又少,无法抓住读者的痛点,能够读完已属不易。
  • 疲劳度是这一系列的一个重要的衡量指标,文章中如果连续出现大段代码,疲劳度会急剧上升,不容易集中精神,甚至中途放弃。

我们期许基于上述的考量和原则,把技术文章写得更加轻松易读,激发读者学习的兴趣,真正实现「上手」。

协程在 Kotlin 中是非常特别的一部分,和 Java 相比,它是一个新颖的概念。宣扬它的人都在说协程是多么好用,但就目前而言不管是官方文档还是网络上的一些文章都让人难以读懂。

造成这种「不懂」的原因和大多数人在初学 RxJava 时所遇到的问题其实是一致的:对于 Java 开发者来说这是一个新东西。下面我们从「协程是什么」开始说起。

协程是什么

协程并不是 Kotlin 提出来的新概念,其他的一些编程语言,例如:Go、Python 等都可以在语言层面上实现协程,甚至是 Java,也可以通过使用扩展库来间接地支持协程。

当在网上搜索协程时,我们会看到:

  • Kotlin 官方文档说「本质上,协程是轻量级的线程」。
  • 很多博客提到「不需要从用户态切换到内核态」、「是协作式的」等等。

作为 Kotlin 协程的初学者,这些概念并不是那么容易让人理解。这些往往是作者根据自己的经验总结出来的,只看结果,而不管过程就不容易理解协程

「协程 Coroutines」源自 Simula 和 Modula-2 语言,这个术语早在 1958 年就被 Melvin Edward Conway 发明并用于构建汇编程序,说明协程是一种编程思想,并不局限于特定的语言。

Go 语言也有协程,叫 Goroutines,从英文拼写就知道它和 Coroutines 还是有些差别的(设计思想上是有关系的),否则 Kotlin 的协程完全可以叫 Koroutines 了。

因此,对一个新术语,我们需要知道什么是「标准」术语,什么是变种。

当我们讨论协程和线程的关系时,很容易陷入中文的误区,两者都有一个「程」字,就觉得有关系,其实就英文而言,Coroutines 和 Threads 就是两个概念。

从 Android 开发者的角度去理解它们的关系:

  • 我们所有的代码都是跑在线程中的,而线程是跑在进程中的。
  • 协程没有直接和操作系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程,也可以是多线程。
  • 单线程中的协程总的执行时间并不会比不用协程少。
  • Android 系统上,如果在主线程进行网络请求,会抛出 NetworkOnMainThreadException,对于在主线程上的协程也不例外,这种场景使用协程还是要切线程的。

协程设计的初衷是为了解决并发问题,让 「协作式多任务」 实现起来更加方便。这里就先不展开「协作式多任务」的概念,等我们学会了怎么用再讲。

视频里讲到,协程就是 Kotlin 提供的一套线程封装的 API,但并不是说协程就是为线程而生的。

不过,我们学习 Kotlin 中的协程,一开始确实可以从线程控制的角度来切入。因为在 Kotlin 中,协程的一个典型的使用场景就是线程控制。就像 Java 中的 Executor 和 Android 中的 AsyncTask,Kotlin 中的协程也有对 Thread API 的封装,让我们可以在写代码时,不用关注多线程就能够很方便地写出并发操作。

在 Java 中要实现并发操作通常需要开启一个 Thread

☕️
new Thread(new Runnable() {
@Override
public void run() {

}
}).start();

这里仅仅只是开启了一个新线程,至于它何时结束、执行结果怎么样,我们在主线程中是无法直接知道的。

Kotlin 中同样可以通过线程的方式去写:

🏝️
Thread({

}).start()

可以看到,和 Java 一样也摆脱不了直接使用 Thead 的那些困难和不方便:

  • 线程什么时候执行结束
  • 线程间的相互通信
  • 多个线程的管理

我们可以用 Java 的 Executor 线程池来进行线程管理:

🏝️
val executor = Executors.newCachedThreadPool()
executor.execute({

})

用 Android 的 AsyncTask 来解决线程间通信:

🏝️
object : AsyncTask<T0, T1, T2> {
override fun doInBackground(vararg args: T0): String { … }
override fun onProgressUpdate(vararg args: T1) { … }
override fun onPostExecute(t3: T3) { … }
}

AsyncTask 是 Android 对线程池 Executor 的封装,但它的缺点也很明显:

  • 需要处理很多回调,如果业务多则容易陷入「回调地狱」。
  • 硬是把业务拆分成了前台、中间更新、后台三个函数。

看到这里你很自然想到使用 RxJava 解决回调地狱,它确实可以很方便地解决上面的问题。

RxJava,准确来讲是 ReactiveX 在 Java 上的实现,是一种响应式程序框架,我们通过它提供的「Observable」的编程范式进行链式调用,可以很好地消除回调。

使用协程,同样可以像 Rx 那样有效地消除回调地狱,不过无论是设计理念,还是代码风格,两者是有很大区别的,协程在写法上和普通的顺序代码类似。

这里并不会比较 RxJava 和协程哪个好,或者讨论谁取代谁的问题,我这里只给出一个建议,你最好都去了解下,因为协程和 Rx 的设计思想本来就不同。

下面的例子是使用协程进行网络请求获取用户信息并显示到 UI 控件上:

🏝️
launch({
val user = api.getUser() // 👈 网络请求(IO 线程)
nameTv.text = user.name // 👈 更新 UI(主线程)
})

这里只是展示了一个代码片段,launch 并不是一个顶层函数,它必须在一个对象中使用,我们之后再讲,这里只关心它内部业务逻辑的写法。

launch 函数加上实现在 {} 中具体的逻辑,就构成了一个协程。

通常我们做网络请求,要不就传一个 callback,要不就是在 IO 线程里进行阻塞式的同步调用,而在这段代码中,上下两个语句分别工作在两个线程里,但写法上看起来和普通的单线程代码一样。

这里的 api.getUser 是一个挂起函数,所以能够保证 nameTv.text 的正确赋值,这就涉及到了协程中最著名的「非阻塞式挂起」。这个名词看起来不是那么容易理解,我们后续的文章会专门对这个概念进行讲解。现在先把这个概念放下,只需要记住协程就是这样写的就行了。

这种「用同步的方式写异步的代码」看起来很方便吧,那么我们来看看协程具体好在哪。

协程好在哪

开始之前

在讲之前,我们需要先了解一下「闭包」这个概念,调用 Kotlin 协程中的 API,经常会用到闭包写法。

其实闭包并不是 Kotlin 中的新概念,在 Java 8 中就已经支持。

我们先以 Thread 为例,来看看什么是闭包:

🏝️
// 创建一个 Thread 的完整写法
Thread(object : Runnable {
override fun run() {

}
})

// 满足 SAM,先简化为
Thread({

})

// 使用闭包,再简化为
Thread {

}

形如 Thread {...} 这样的结构中 {} 就是一个闭包。

在 Kotlin 中有这样一个语法糖:当函数的最后一个参数是 lambda 表达式时,可以将 lambda 写在括号外。这就是它的闭包原则。

在这里需要一个类型为 Runnable 的参数,而 Runnable 是一个接口,且只定义了一个函数 run,这种情况满足了 Kotlin 的 SAM,可以转换成传递一个 lambda 表达式(第二段),因为是最后一个参数,根据闭包原则我们就可以直接写成 Thread {...}(第三段) 的形式。

对于上文所使用的 launch 函数,可以通过闭包来进行简化 :

🏝️
launch {

}

基本使用

前面提到,launch 函数不是顶层函数,是不能直接用的,可以使用下面三种方法来创建协程:

🏝️
// 方法一,使用 runBlocking 顶层函数
runBlocking {
getImage(imageId)
}

// 方法二,使用 GlobalScope 单例对象
// 👇 可以直接调用 launch 开启协程
GlobalScope.launch {
getImage(imageId)
}

// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
// 👇 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
getImage(imageId)
}

  • 方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。

  • 方法二和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和 app 一致,且不能取消(什么是协程的取消后面的文章会讲)。

  • 方法三是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。

关于 CoroutineScopeCoroutineContext 的更多内容后面的文章再讲。

协程最常用的功能是并发,而并发的典型场景就是多线程。可以使用 Dispatchers.IO 参数把任务切到 IO 线程执行:

🏝️
coroutineScope.launch(Dispatchers.IO) {

}

也可以使用 Dispatchers.Main 参数切换到主线程:

🏝️
coroutineScope.launch(Dispatchers.Main) {

}

所以在「协程是什么」一节中讲到的异步请求的例子完整写出来是这样的:

🏝️
coroutineScope.launch(Dispatchers.Main) { // 在主线程开启协程
val user = api.getUser() // IO 线程执行网络请求
nameTv.text = user.name // 主线程更新 UI
}

而通过 Java 实现以上逻辑,我们通常需要这样写:

☕️
api.getUser(new Callback() {
@Override
public void success(User user) {
runOnUiThread(new Runnable() {
@Override
public void run() {
nameTv.setText(user.name);
}
})
}

@Override
public void failure(Exception e) {

}
});

这种回调式的写法,打破了代码的顺序结构和完整性,读起来相当难受。

协程的「1 到 0」

对于回调式的写法,如果并发场景再复杂一些,代码的嵌套可能会更多,这样的话维护起来就非常麻烦。但如果你使用了 Kotlin 协程,多层网络请求只需要这么写:

🏝️
coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程
val token = api.getToken() // 网络请求:IO 线程
val user = api.getUser(token) // 网络请求:IO 线程
nameTv.text = user.name // 更新 UI:主线程
}

如果遇到的场景是多个网络请求需要等待所有请求结束之后再对 UI 进行更新。比如以下两个请求:

🏝️
api.getAvatar(user, callback)
api.getCompanyLogo(user, callback)

如果使用回调式的写法,那么代码可能写起来既困难又别扭。于是我们可能会选择妥协,通过先后请求代替同时请求:

🏝️
api.getAvatar(user) { avatar ->
api.getCompanyLogo(user) { logo ->
show(merge(avatar, logo))
}
}

在实际开发中如果这样写,本来能够并行处理的请求被强制通过串行的方式去实现,可能会导致等待时间长了一倍,也就是性能差了一倍。

而如果使用协程,可以直接把两个并行请求写成上下两行,最后再把结果进行合并即可:

🏝️
coroutineScope.launch(Dispatchers.Main) {
// 👇 async 函数之后再讲
val avatar = async { api.getAvatar(user) } // 获取用户头像
val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
val merged = suspendingMerge(avatar, logo) // 合并结果
// 👆
show(merged) // 更新 UI
}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码。需要注意的是 suspendingMerge 并不是协程 API 中提供的方法,而是我们自定义的一个可「挂起」的结果合并方法。至于挂起具体是什么,可以看下一篇文章。

让复杂的并发代码,写起来变得简单且清晰,是协程的优势。

这里,两个没有相关性的后台任务,因为用了协程,被安排得明明白白,互相之间配合得很好,也就是我们之前说的「协作式任务」。

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

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

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

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

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

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

最后说一下我的学习路线

其实很简单就下面这张图,含概了Android所有需要学的知识点,一共8大板块:

  1. 架构师筑基必备技能
  2. Android框架体系架构(高级UI+FrameWork源码)
  3. 360°Androidapp全方位性能调优
  4. 设计思想解读开源框架
  5. NDK模块开发
  6. 移动架构师专题项目实战环节
  7. 移动架构师不可不学习微信小程序
  8. 混合开发的flutter

Android学习的资料

我呢,把上面八大板块的分支都系统的做了一份学习系统的资料和视频,大概就下面这些,我就不全部写出来了,不然太长了影响大家的阅读。

330页PDF Android学习核心笔记(内含上面8大板块)

Android学习的系统对应视频

总结

我希望通过我自己的学习方法来帮助大家去提升技术:

  • 1、多看书、看源码和做项目,平时多种总结

  • 2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理

  • 3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习

  • 4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!

希望大家多多点赞,转发,评论加关注,你们的支持就是我继续下去的动力!加油!

Android学习的系统对应视频

总结

我希望通过我自己的学习方法来帮助大家去提升技术:

  • 1、多看书、看源码和做项目,平时多种总结

  • 2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理

  • 3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习

  • 4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!

希望大家多多点赞,转发,评论加关注,你们的支持就是我继续下去的动力!加油!

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值