Kotlin 已经成为Android开发的主打语言好些年了。但是我们在使用Kotlin时,不要仅限于使用Kotlin的语法糖。还有要使用其更有技术含量的API。比如这篇文章要讲的内容--协程
在介绍协程之前,先说一下 Android 里面开启异步功能(开启新线程)的方法,要么开启一个新的Thread,要么创建一个线程池,要么创建HandlerThread,要么使用 IntentService。其实这几种方法,归根结底,都是开启了一个新的线程。新开一个线程,会消耗比较大的内存。频繁的开启线程的话还会导致内存抖动。而且,开启的线程,要销毁和监听何时结束和执行结果,也是需要比较大的工作量。
比如下面,开启线程何时结束,何时有结果我们无法感知。而且开启一个线程所需要的内存也是很大。
Thread {
// do something
}.start()
还有,当新开启线程执行功能时,需要线程的切换,线程A交出CPU执行权和线程B获取CPU执行权,这就涉及到线程之间的停止和开启,消耗内存太大了。
在这样的情况,这篇文章的主角 协程 出现了,协程既解决开启线程内存消耗大的问题,也在使用上提供销毁,何时结束等API,使用起来很方便...
首先在定义上说明一下,
线程是依赖于系统的,一个系统有多个线程。
协程是依赖于线程的,一个线程有多个协程。
也就是,协程在哪个线程开启,它就依赖于这个线程,这个线程就是他的宿主线程
首先新开启的 协程 为什么能大大节省内存呢?先从原理上讲,每一个协程,就相当于一个功能块(Code Block) ,这些功能块可以指定在哪一个线程执行。如果指定在主线程执行,那就把这个功能块放在主线程里面执行,如果指定在IO线程执行,那就放在IO线程里面去执行。如果多个功能块放在IO线程里面,那就一个等待一个排队去执行,如果某个协程需要睡眠,线程就会跳过该协程去执行下一个协程,直到该协程唤醒再执行。所以,协程在创建时,并没有新建起新的线程,它只是在共享线程池中取出。所以在内存消耗上很小。如下图,Thread A 添加了多个协程,一个挨着一个去执行...
在Android 官方文档里标注,开启一个线程消耗内存远大于开启一个协程。
使用之前,先配置一下Gradle
dependencies {
// 👇 依赖协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1"
// 👇 依赖当前平台所对应的平台库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1"
}
话不多说,开启协程的方式有很多种,先上第一种
==========================================================================
第一种: 阻塞型 runBlocking(不推荐)
==========================================================================
这是一种不推荐的开启协程的方式,因为这种会阻塞线程。启动一个新的协程,并阻塞它的调用线程,直到里面的代码执行完毕。
其实在业务开发过程中,也没有多少这种需求需要阻塞线程的。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutines)
val startTime = System.currentTimeMillis()
Log.d(TAG, "开始获取网络数据...time: $startTime")
runBlocking {
val text = getText()
Log.d(TAG, "网络数据...$text time: ${System.currentTimeMillis() - startTime} currentThread:${Thread.currentThread().name}")
}
Log.d(TAG, "执行完毕... time: ${System.currentTimeMillis() - startTime}")
}
private suspend fun getText(): String {
Thread.sleep(3000)
return "网络数据..."
}
看日志分析:
很明显,在主线程回调,runBlocking 阻塞了线程。这是一种不推荐开启协程的方式
其实这种方式,和不开启协程是没有去区别,因为上述功能直接写在协程外面同样阻塞主线程。
当然,runBlocking 是可以指定线程,不过同样会阻塞其依赖的线程
=========================================================================
第二种:launch 和 async(不推荐)
=========================================================================
launch 和 async 是必须在 协程里面开启 才能编译过去,在协程之外开启,编译会报错
只有写在协程里面才能编译过去
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutines)
runBlocking {
Log.d(TAG, "runBlocking... currentThread:${Thread.currentThread().name}")
launch {
Log.d(TAG, "launch... currentThread:${Thread.currentThread().name}")
val text = getText()
}
}
}
看下日志
分析日志 :
runBlocking 开启了一个协程,在该协程内部 又用 launch 开启了一个协程,由于launch 出来的协程并没有指定在哪个线程执行,它会默认在跟随启动它的那个协程。所以runBlocking 在主线程,launch 出来的协程也在主线程。
当然 launch 是可以指定线程,比如:
Dispatchers.Default 默认线程
Dispatchers.IO IO线程,网络请求,IO流读写
Dispatchers.Main 主线程
Dispatchers.Unconfined 不指定,默认当前线程
我们分别试着分别指定四种,会有什么效果:
Dispatchers.Default 默认线程
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutines)
runBlocking {
Log.d(TAG, "runBlocking... currentThread:${Thread.currentThread().name}")
launch(Dispatchers.Default) {
Log.d(TAG, "launch... currentThread:${Thread.currentThread().name}")
val text = getText()
}
}
}
看下日志:
Default 就是协程默认在共享线程池中的一个 名为:worker(非主线程)的线程中执行
Dispatchers.IO IO线程,网络请求,IO流读写
runBlocking {
Log.d(TAG, "runBlocking... currentThread:${Thread.currentThread().name}")
launch(Dispatchers.IO) {
Log.d(TAG, "launch... currentThread:${Thread.currentThread().name}")
val text = getText()
}
}
看下日志:
IO 和 Default 一样, 就是协程默认在共享线程池中的一个 名为:worker(非主线程)的线程中执行
Dispatchers.Main 主线程
runBlocking {
Log.d(TAG, "runBlocking... currentThread:${Thread.currentThread().name}")
launch(Dispatchers.Main) {
Log.d(TAG, "launch... currentThread:${Thread.currentThread().name}")
val text = getText()
}
}
其实这个不用看就知道了,,,,,
Dispatchers.Unconfined 不指定,默认当前线程
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutines)
runBlocking {
Log.d(TAG, "runBlocking... currentThread:${Thread.currentThread().name}")
launch(Dispatchers.Unconfined) {
Log.d(TAG, "launch... currentThread:${Thread.currentThread().name}")
}
}
}
看下日志
其实这个和不指定线程是一样。
async 和 launch 和差不多,就是比 launch多了一个返回值。返回值 写在闭包 {}最后一行即可,然后通过 await()获取结果
比如:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutines)
runBlocking {
Log.d(TAG, "runBlocking... currentThread:${Thread.currentThread().name}")
val job = async(Dispatchers.IO) {
Log.d(TAG, "launch... currentThread:${Thread.currentThread().name}")
23
}
val await = job.await()
Log.d(TAG, "async... 运行结果:$await")
}
}
看日志:
===========================================================================
第三种:GlobalScope.launch 和 GlobalScope.async 全局单例(不推荐)
===========================================================================
这种开启协程的方式,不会阻塞线程
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutines)
val startTime = System.currentTimeMillis()
Log.d(TAG, "开始获取网络数据...time: $startTime")
GlobalScope.launch {
val name = Thread.currentThread().name
val text = getText()
Log.d(TAG, "网络数据...$text time: ${System.currentTimeMillis() - startTime} currentThread: $name")
}
Log.d(TAG, "执行完毕... time: ${System.currentTimeMillis() - startTime} currentThread:${Thread.currentThread().name}")
}
private suspend fun getText(): String {
Thread.sleep(3000)
return "网络数据..."
}
分析日志:
从日志分析得:通过 GlobalScope 开启单例协程,并没有阻塞线程,但是GlobalScope 开启单例协程相比于runBlocking ,只是没有阻塞线程,并没有多大优势,因为它是全局的单例,跟随application生命周期,不能取消。
GlobalScope.async 相比于 GlobalScope.launch 就是多了一个返回值功能,比如:
runBlocking {
val job = GlobalScope.async {
Log.d(TAG, "async... Thread:${Thread.currentThread().name}")
1212
}
val await = job.await()
Log.d(TAG, "async... 运行结果:$await")
}
看日志:
最后,对比GlobalScope.launch 和 launch 的却别,前者开启的是一个全局协程,生命周期和Application一样,后者会随着外部协程的销毁而销毁。
/**
* 外部协程 1
* */
runBlocking(Dispatchers.IO) {
/**
* 外部协程 2
* */
val job = launch {
/**
* 内部协程 1 GlobalScope.launch
* */
GlobalScope.launch {
Log.d(TAG, "GlobalScope.launch... Thread:${Thread.currentThread().name}")
delay(1500)
val text = getText()
Log.d(TAG, "async... 获取结果:${text}")
}
/**
* 内部协程 1 launch
* */
launch {
Log.d(TAG, "launch... Thread:${Thread.currentThread().name}")
delay(1500)
val text = getText()
Log.d(TAG, "launch... 获取结果:${text}")
}
}
delay(1000)
job.cancel()
Log.d(TAG, "外部协程取消...")
delay(2000)
}
看日志分析:
最开始,GlobalScope.launch 和 launch 都有执行,但是外部外部协程取消后,GlobalScope.launch 没有受外部协程影响,依然执行完协程功能。 launch 随着外部协程一同销毁了。
===========================================================================
第四种:CoroutineScope (推荐)
===========================================================================
private fun testScope() {
val coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope.launch(Dispatchers.IO) {
val text = getText()
coroutineScope.launch(Dispatchers.Main) {
tv1.text = text
}
}
}
private suspend fun getText(): String {
Thread.sleep(3000)
return "网络数据..."
}
这一种推荐的。由CoroutineScope 创建, 由 Dispatchers 指定在(Dispatchers.Main )主线程。
========================================
suspend 关键词
========================================
private suspend fun getText(): String {
Thread.sleep(3000)
return "网络数据..."
}
suspend 关键词 修饰的方法,只能在协程里面调用,因为suspend 修饰的方法,被调用后,就代表这个协程被挂起。挂起的意思,就是协程里面的代码会切换到指定的线程去执行。等执行完后切回到当前线程继续执行。但是协程之外的代码,会继续执行,所以不会阻塞到当前线程。
========================================
协程的异常捕抓
========================================
private fun testScope() {
val coroutineScope = CoroutineScope(Dispatchers.Main + defaultExceptionHandler)
coroutineScope.launch(Dispatchers.IO) {
val text = getText()
coroutineScope.launch(Dispatchers.Main) {
tv1.text = text
}
}
}
private suspend fun getText(): String {
Thread.sleep(3000)
return "网络数据..."
}
private val defaultExceptionHandler = object : CoroutineExceptionHandler {
override val key: CoroutineContext.Key<*> = CoroutineExceptionHandler
override fun handleException(context: CoroutineContext, exception: Throwable) {
//捕获异常 do something
}
}
========================================
协程的取消1:CoroutineScope.cancel()
========================================
private fun testScope() {
val coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope.launch(Dispatchers.IO) {
val text = getText()
coroutineScope.launch(Dispatchers.Main) {
tv1.text = text
}
}
coroutineScope.cancel()
}
private suspend fun getText(): String {
Thread.sleep(3000)
return "网络数据..."
}
cancel() 取消当前协程。
========================================
协程同步多个任务
========================================
说个题外话,协程没出来前,合并多个异步任务,一般都是使用RxJava的zip方法的
Observable.zip(),每一个异步任务都是一个 ObservableSource,最后把几个 ObservableSource绑在一个 BiFunction 里面回调。那里面的写法,真是地狱级别的回调,代码阅读性极差
至于协程的是怎么做的,废话少说,直接上代码
private fun test() {
lifecycleScope.launch {
val startTime = System.currentTimeMillis()
Log.d(TAG, "2个异步任务开始:${startTime}")
val mergerData = withContext(Dispatchers.IO) {
//任务1,通过 async 必包实现
val data1 = async {
getData1()
}
//任务2,通过 async 必包实现
val data2 = async {
getData2()
}
// 最后一行,就是返回的数据,类型可以随便定义
// 通过await(),获取各自任务的结果,只有等两个异步任务都结束,才会返回数据
"合并两个数据: ${data1.await()}, ${data2.await()}"
}
val endTime = System.currentTimeMillis()
Log.d(TAG,"2个异步任务结束,耗时:${endTime - startTime},结果:${mergerData}")
}
}
private suspend fun getData1(): String {
delay(5000)
Log.d(TAG, "返回Data1")
return "Data1"
}
private suspend fun getData2(): String {
delay(10000)
Log.d(TAG, "返回Data2")
return "Data2"
}
看下运行结果:
任务1耗时5秒,任务2耗时10秒,把两任务合并在一起,不是5秒,不是15秒,是10秒了。
这就验证了,两个异步任务是并行执行的。
最后一行代码,就是两个异步任务返回的结果,类型随便定义。
任务获取结果,需要await()方法。
========================================
协程的取消2:绑定类的生命周期 CoroutineScope by MainScope()
========================================
class MyCoroutineActivity : AppCompatActivity(), CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my_coroutine)
runBlocking(Dispatchers.IO) {
launch(Dispatchers.IO) {
}
}
}
override fun onDestroy() {
/**
* 取消 Activity 里面的所有协程
* */
cancel()
super.onDestroy()
}
}
CoroutineScope by MainScope() 实现了该接口的Activity,onDestory()销毁时 调用 cancel()即可取消在 该Activity创建的协程。
========================================
协程的取消2:自定义 MainScope()
========================================
class MyCoroutineActivity : AppCompatActivity() {
private var mainScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my_coroutine)
mainScope.launch {
}
mainScope.async {
"返回的数据"
}
}
override fun onDestroy() {
/**
* 取消 MainScope 创建所有协程
* */
mainScope.cancel()
super.onDestroy()
}
}
自定义 MainScope() ,onDestory()销毁时 调用 MainScope().cancel() 即可取消协程。
2021.6.3 更新
===========================================================================
第5种:lifecycleScope(推荐)
===========================================================================
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
依赖在Activty,Fragment,ViewModel层面提供协程实例,协程与其声明周期绑定,无须手动创建和取消销毁
Activity
class CoroutinesActivity : AppCompatActivity() {
companion object {
fun launch(context: Context) {
context.startActivity<CoroutinesActivity>()
}
const val TAG = "CoroutinesTag"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coroutines)
val beginTransaction = supportFragmentManager.beginTransaction()
beginTransaction.add(R.id.container, CoroutinesFragment()).commit()
lifecycleScope.launch(Dispatchers.IO) {
Log.i("MyTest", "CoroutinesActivity ${Thread.currentThread().name}")
}
}
}
Fragment
class CoroutinesFragment : Fragment() {
private lateinit var coroutinesVM: CoroutinesVM
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.coroutines_fragment_layout, container, false)
}
override fun onResume() {
super.onResume()
coroutinesVM =
ViewModelProvider(this, defaultViewModelProviderFactory).get(CoroutinesVM::class.java)
coroutinesVM.launchCoroutines()
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
Log.i("MyTest", "CoroutinesFragment ${Thread.currentThread().name}")
}
}
}
ViewModel
class CoroutinesVM : ViewModel() {
fun launchCoroutines() {
viewModelScope.launch(Dispatchers.Default) {
Log.i("MyTest", "CoroutinesVM ${Thread.currentThread().name}")
}
}
}
启动Activity,看日志:
OK!!!!!!
下面我给三个协程加入10秒延时,然后进入页面后立马关闭(即延时还没结束),看下是否会打印日志:
lifecycleScope.launch(Dispatchers.IO) {
delay(10 * 1000)
Log.i("MyTest", "CoroutinesActivity ${Thread.currentThread().name}")
}
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
delay(10 * 1000)
Log.i("MyTest", "CoroutinesFragment ${Thread.currentThread().name}")
}
fun launchCoroutines() {
viewModelScope.launch(Dispatchers.Default) {
delay(10 * 1000)
Log.i("MyTest", "CoroutinesVM ${Thread.currentThread().name}")
}
}
运行结果是没有任何日志打印。
说明了并不需要自己手动取消协程,开的协程会和Activty,Fragment,ViewModel生命周期绑定的。
所以推荐使用最后这一种
以上代码亲测没问题,有问题请指出,谢谢