Bootstrap

Kotlin协程使用

一、开启协程的方式

1、顶层开启协程的函数

基本使用:

// 方法一,使用 runBlocking 顶层函数
runBlocking {
    getImage(imageId)
}
​
// 方法二,使用 GlobalScope 单例对象
//            👇 可以直接调用 launch 开启协程,或者aysnc需要await接收结果
GlobalScope.launch {
    getImage(imageId)
}
​
// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
//                                    👇 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch { //或者async,需要await接收结果
    getImage(imageId)
}
  • 方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。

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

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

以上三个方法的共同特点就是,在非协程的代码中开启协程的方法。

2、切协程方法/操作符

操作符/函数描述
withContext这个函数可以在指定的上下文中执行协程代码。例如,可以使用Dispatchers.IO来在IO线程中执行协程。
launch用于启动一个新的协程。它会返回一个Job对象,可以用来取消协程
asynclaunch类似,但它会返回一个Deferred对象,可以通过调用await()方法获取协程的结果

注意:launch 和 async 是必须在 协程里面开启 才能编译过去,在协程之外开启,编译会报错;withContext则必须在协程或者suspend修饰的方法中使用,否则编译报错。

launch和withContext的区别

在Kotlin协程中,launchwithContext是两个用于启动协程的函数,它们之间存在一些差异。具体分析如下:

  • launchlaunch函数用于在协程作用域内创建一个新的协程。它不阻塞当前线程,而是将协程任务放入后台执行。launch通常用于执行不需要返回值的任务,例如在后台执行一些操作而不需要等待结果。launch函数的特点是它会将协程任务挂起,直到任务完成或者被取消。

  • withContextwithContext函数用于在特定的上下文中执行协程代码块。它可以确保代码块在指定的协程作用域内执行,并且可以使用指定的协程调度器或Job。withContext通常用于需要切换协程作用域或调度器的场景,例如在IO密集型任务中使用Dispatchers.IO调度器。

import kotlinx.coroutines.*
​
fun main() = runBlocking {
    val job = launch {
        // 在这里执行协程代码
    }
    // 等待协程执行完成
    job.join()
}
​
fun main() = runBlocking {
    val deferred = async {
        // 在这里执行协程代码并返回结果
    }
    // 获取协程的结果
    val result = deferred.await()
    println("Result: $result")
}
​
fun main() = runBlocking {
    withContext(Dispatchers.IO) {
        // 在这里执行协程代码
    }
}
​
 fun test2(){
     val job = GlobalScope.launch {
         // 在这里执行协程代码
         print(t1()+t2())
     }
     Thread.sleep(3000)
     print("After runBlocking ${job.isActive} ")
 }
suspend fun t1() =  withContext(Dispatchers.IO){
    delay(1000L) // 模拟耗时操作
    "After delay 1 "
}
suspend fun t2() = withContext(Dispatchers.IO){
    delay(2000L) // 模拟耗时操作
    "After delay 2"
}
​

二、Android中开启协程

在Android Activity中开启协程,通常需要结合CoroutineScope和生命周期感知组件(如lifecycleScope)来确保协程的生命周期与Activity的生命周期自动绑定

1、使用lifecycleScope:

lifecycleScope是一个与Activity或Fragment的生命周期绑定的特殊CoroutineScope。当UI组件被销毁时,所有在这个范围内启动的协程都会自动取消,防止内存泄漏。

import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
​
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)
        
        // 使用lifecycleScope启动协程
        lifecycleScope.launch {
            // 在这里执行协程代码
        }
    }
}

2、 手动管理协程作用域:

如果你想更灵活地管理协程,可以创建一个自定义的CoroutineScope并显式地绑定到LifecycleOwner,例如ActivityFragment

import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.*
​
class MyActivity : AppCompatActivity() {
    private lateinit var job: Job
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)
        
        // 创建一个新的协程作用域
        val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
        
        // 绑定协程作用域到Activity生命周期
        job = coroutineScope.launch {
            // 在这里执行协程代码
        }
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // 取消协程
        job.cancel()
    }
}

3、使用ViewModel和LiveData:

如果你正在使用ViewModelLiveData架构组件,可以在ViewModel中启动协程,并通过LiveData将结果传递给UI层。

import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asLiveData
​
class MyActivity : AppCompatActivity() {
    private lateinit var viewModel: MyViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)
        
        // 初始化ViewModel
        viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
        
        // 观察LiveData的变化
        viewModel.resultLiveData.observe(this, Observer { result ->
            // 更新UI界面
        })
        
        // 请求数据
        viewModel.fetchData()
    }
}
​
import androidx.lifecycle.*
import kotlinx.coroutines.*
​
class MyViewModel(private val repository: MyRepository) : ViewModel() {
    val resultLiveData = MutableLiveData<String>()
    
    fun fetchData() {
        // 在ViewModel中启动协程
        viewModelScope.launch {
            // 从仓库获取数据
            val result = repository.getDataFromApi()
            if (result is Success) {
                // 设置LiveData的值
                resultLiveData.value = result.data
            } else {
                // 处理错误
            }
        }
    }
}
​

在上述代码中,viewModelScope是专为ViewModel设计的CoroutineScope,它会在ViewModel清除时自动取消协程。这确保了即使ActivityFragment被销毁,协程也不会继续运行,从而避免内存泄漏。

4、lifecycleScope.launch、GlobalScope.launch和CoroutineScope(Dispatchers.IO).launch 区别

GlobalScope.launch和CoroutineScope(Dispatchers.IO).launch在协程的生命周期、调度器选择以及使用场景上有所区别。具体分析如下:

  1. 生命周期:使用GlobalScope.launch启动的协程会持续存在直到明确取消,它与应用程序的生命周期绑定,即使离开原始作用域(如Activity或Fragment)也不会自动取消。这可能导致内存泄漏的风险,因为它不会随着UI组件的销毁而自动清理。而CoroutineScope(Dispatchers.IO).launch创建的协程则通常用于执行后台任务,并且需要显式管理其生命周期,例如在UI组件销毁时取消协程来避免内存泄漏。

  2. 调度器选择GlobalScope.launch默认使用Dispatchers.Default作为调度器,这意味着它会在后台线程池中执行协程,但这并非专为IO密集型任务设计。如果要在IO操作中使用协程而不阻塞线程,应使用Dispatchers.IO调度器。CoroutineScope(Dispatchers.IO).launch则直接指定了协程应该在Dispatchers.IO调度器上运行,这是专门用于IO操作的线程池,能够提供更好的性能。

  3. 使用场景GlobalScope.launch适用于全局范围内长期存在的后台任务,但需谨慎使用以避免内存泄漏。相比之下,CoroutineScope(Dispatchers.IO).launch更适用于临时性的、与特定任务关联的后台工作,比如网络请求或数据库操作,这些任务通常需要在用户界面相关的生命周期内完成。

总的来说,CoroutineScope(Dispatchers.IO).launch提供了更多的控制和灵活性,尤其是在需要精细管理协程生命周期的情况下。而GlobalScope.launch虽然使用起来简单,但可能会引起内存泄漏等问题,因此在实际开发中应当谨慎使用。

5、lifecycleScope.launch{}与 viewModelScope.launch{} 的区别

lifecycleScope.launchviewModelScope.launch确实存在差异。具体分析如下:

  • lifecycleScope.launchlifecycleScope是与LifecycleOwner(如ActivityFragment或任何实现了Lifecycle的组件)的生命周期绑定的协程作用域。当LifecycleOwner被销毁时,在这个作用域内启动的所有协程都会自动取消。这有助于避免因忘记取消协程而导致的内存泄漏问题。此外,lifecycleScope默认在主线程中启动协程,但也提供了其他扩展方法,如launchWhenCreatedlaunchWhenStartedlaunchWhenResumed等,这些方法允许你根据组件的特定生命周期状态来启动协程。

  • viewModelScope.launchviewModelScope是与ViewModel的生命周期绑定的协程作用域。当ViewModel清除时,这个作用域内启动的所有协程也会被自动取消。这对于需要在ViewModel生命周期内执行的后台任务非常有用,例如网络请求或数据库操作。viewModelScope有助于确保当ViewModel不再需要时,相关的后台工作也会被清理,从而避免潜在的内存泄漏。需要注意的是,viewModelScope是在AndroidX Lifecycle v2.1.0(alpha阶段)中引入的,因此API可能会发生变化。

总结来说,lifecycleScope.launch适用于与UI组件的生命周期紧密相关的场景,而viewModelScope.launch适用于与ViewModel的生命周期紧密相关的场景。两者都提供了自动取消协程的机制,以帮助开发者管理协程的生命周期并防止内存泄漏。在实际开发中,选择哪种方式取决于你的后台任务需要与哪种组件的生命周期绑定。

6、launchWhenStarted创建的协程什么时机销毁

launchWhenStarted创建的协程会在LifecycleOwner(如ActivityFragment)的生命周期状态从STARTED变为DESTROYED时被取消。

具体来说,launchWhenStartedlifecycleScope的一个扩展方法,它会延迟协程的启动直到LifecycleOwner进入STARTED状态。一旦LifecycleOwner的生命周期状态从STARTED变为DESTROYED,即组件被销毁时,由launchWhenStarted创建的协程将自动被取消。

这种机制的好处在于:

  • 自动管理协程:开发者不需要手动管理协程的生命周期,避免了忘记取消协程可能导致的内存泄漏问题。

  • 资源利用效率:与lifecycleScope.launch相比,launchWhenStarted不会在LifecycleOwner的整个生命周期内运行协程,而是在组件开始和销毁之间,这样可以更有效地利用资源,避免不必要的后台任务执行。

  • 安全性:由于协程会在组件销毁时自动取消,这增加了应用程序的稳定性和安全性。

总的来说,使用lifecycleScope.launchWhenStarted可以帮助开发者在Android应用中更加安全和高效地执行与组件生命周期相关的后台任务。

7、MainScope介绍

  • 实际项目中的使用法一
class MainActivity : AppCompatActivity() {
​
    private var mainScope = MainScope()
​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
​
        test1()
//        test2()
//        test3()
//        test4()
    }
​
    private fun test1() {
        mainScope.launch(Dispatchers.IO) {
            val text = getText()
            Log.i("cyc", "IO:")
            mainScope.launch(Dispatchers.Main) {
                Log.i("cyc", "Main:")
                tv1.text = text
            }
        }
    }
​
    private suspend fun getText(): String {
        Thread.sleep(3000)
        return "网络数据..."
    }
​
    private fun test2() {
        lifecycleScope.launch(Dispatchers.IO) {
            val lg = async(Dispatchers.IO) {
                Log.i("cyc", "请求网络IO")
            }
            lg.await().run {
                Log.i("cyc", "逻辑处理")
            }
        }
    }
​
    private fun test3() {
        lifecycleScope.launch(Dispatchers.Default) {
            delay(3000L)
            withContext(Dispatchers.Main) {
                Toast.makeText(this@MainActivity, "aa", Toast.LENGTH_SHORT).show()
            }
            delay(2000L)
            withContext(Dispatchers.Main) {
                Toast.makeText(this@MainActivity, "bb", Toast.LENGTH_SHORT).show()
            }
        }
    }
​
    private fun test4() {
        lifecycleScope.launch(Dispatchers.Default) {
            startPolling(3000L) {
                Log.i("cyc", "startPolling: ")
            }
        }
    }
​
    private suspend fun startPolling(intervals: Long, block: () -> Unit) {
        flow {
            while (true) {
                delay(intervals)
                emit(0)
            }
        }
            .catch { Log.e("cyc", "startPolling--catch: $it") }
            .flowOn(Dispatchers.IO)//作用于flow代码块
            .collect {
                withContext(Dispatchers.Main) {
                    block.invoke()
                }
            }
    }
​
    override fun onDestroy() {
        super.onDestroy()
        mainScope.cancel()
​
    }
}
 
  • 实际项目中的使用法二

通过CoroutineScope by MainScope(),在需要的地方直接cancle

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()
    }
}
​

三、执行多个并行异步操作,所有异步结束后执行合并方案:

1、传统java方案:

import java.util.concurrent.CountDownLatch;
​
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        final CountDownLatch countDownLatch = new CountDownLatch(2);
        int[] data = new int[2];
​
        for (int i = 0; i < 2; i++) {
            int finalI = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "start running");
                try {
                    data[finalI] = finalI;
                    Thread.sleep(finalI * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "complete-----" + System.currentTimeMillis() + ",finalI:" + finalI);
                countDownLatch.countDown();
            }).start();
        }
        System.out.println("waiting for all thread to complete... ");
        countDownLatch.await();
        int sum = 0;
        for (int datum : data) {
            sum += datum;
        }
        System.out.println("When all threads are executed, the main thread continues to execute" + "++++++" + countDownLatch.getCount() + "," + System.currentTimeMillis() + "," + sum);
    }
}

2、协程方案

CoroutineScope(Dispatchers.IO).launch {
    // 在协程中执行异步任务
    val result = async(Dispatchers.IO) { fetchDataFromApi(1) }
    val result2 = async(Dispatchers.IO){ fetchDataFromApi(4) }
    Log.d("TAG", "launch: ${Thread.currentThread()}")
    updateUIWithResult(result.await() + result2.await())
}
 private suspend fun fetchDataFromApi(time: Int = 1): String {
     // 模拟网络请求的延迟
     delay(1000L * time)
     Log.d("TAG", "fetchDataFromApi: ${Thread.currentThread()}")
     return "Hello, Coroutines!"
 }

;