重拾 Kotlin 协程——结构化并发(2)

1,652 阅读4分钟

取消协程

我们都知道,通过以下代码就可以启动一个协程进行运行:

    CoroutineScope(Dispatchers.IO).launch {
    }

但是,这个跟我们启动线程一样,具有一个很大的问题,就是不可控性,我们知道它什么时候启动,却不知道什么时候销毁,就像我们跳转在一个页面进行数据加载的时候,可以使用协程去加载,但是,我们也希望当该页面销毁的时候,该协程也能够销毁。

其实,协程也提供这样的一种方式:

class MainActivity : AppCompatActivity() {
    var job: Job? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        println("onCreate 正在执行")
        job = CoroutineScope(Dispatchers.IO).launch {
            for(index in 0..10000000000){}
            println("Dispatchers.IO 执行完成")
        }
    }

    override fun onDestroy() {
        println("onDestroy 正在执行")
        job?.cancel()

        super.onDestroy()
    }
}

其实就是把 CoroutineScope(Dispatchers.IO).launch 返回的 Job 对象保存起来,然后适时调用其 cancel() 方法。这样就可以取消该线程了。

我们来看下输出:

I/System.out: onCreate 正在执行
I/System.out: onDestroy 正在执行
I/System.out: Dispatchers.IO 执行完成

这是什么回事?不是说好可以取消的吗?怎么没有取消,还把任务执行完了???

其实,这个不难理解,因为 cancel() 的调用其实更像标识符,并不会真正的取消协程,原理跟这个类似 sleep()为什么要 try catch,有兴趣的可以看下。

所以,我们只需要这样更改即可:

    job = CoroutineScope(Dispatchers.IO).launch {
        for(index in 0..10000000000){}
        if (isActive){
            println("Dispatchers.IO 执行完成")
        }
    }

不过,这里又引出了另外一个问题,既然协程在适时需要取消,那有没有一种方式,像 LiveData 一样,能够自动监听生命周期,让其自动取消?

答案当然使用有的,而且 Google 官方还有几种方式,分别为:

  • GlobalScope
  • MainScope
  • LifecycleScope
  • ViewModelScope
  • LiveData

GlobalScope

GlobalScope 相当于 CoroutineScope(Dispatchers.Default),会自动开启一个线程去执行协程里面的代码。

使用方式为:

GlobalScope.launch{  }

所以,若要暂停该协程,还是需要存储 Job 对象,然后调用 cancel() 方法。

MainScope

MainScope 可以理解为 CoroutineScope(Dispatchers.Main) ,所以,也可以通过以下方式进行使用:

MainScope().launch {  }

不过,以这种方式进行调用,在 Activity 销毁的时候,其实也不会去取消协程,若要取消协程,还是得存储 Job 对象,然后调用 cancel() 方法。

为了方便调用 cancel() ,我们可以考虑将其放在基类中,例如 BaseActivity 中:

class BaseActivity : CoroutineScope by MainScope(){
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope(){

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        launch {
            if (isActive) { }
        }
    }

    override fun onDestroy() {
        cancel()
        super.onDestroy()
    }
}

LifecycleScope

使用 MainScope 能够比较方便的调用 cancel(),但是,在含有生命周期的对象时,其实,我们更希望它自动去取消协程。而这时,我们可以考虑使用 LifecycleScope

LifecycleScope 的作用域默认是在主线程中,所以,我们可以把 LifecycleScope 理解为监听 LifeCycle + CoroutineScope(Dispatchers.Main)

使用 LifecycleScope 首先要添加依赖:

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'

然后我们就可以直接在 AppCompatActivity 和 Fragment 中使用 lifecycleScope 开启协程:

lifecycleScope.launch { }

也可以使用 LifecycleOwner.lifecycleScope,用法都是一样的,开启协程后,都无需自己去取消该协程,当 LifeCycle 回调 onDestroy() 的时候会自动取消协程。

当然,若想要自己去取消协程,也无需自己去保存 Job 对象去取消,而是直接这样调用即可:

lifecycleScope.cancel()

另外,lifecycleScope 还有一种特殊的用法,就是可以指定在什么哪个生命周期方法执行完后再 launch,使用方式:

    lifecycleScope.launchWhenCreated {  }
    lifecycleScope.launchWhenStarted {  }
    lifecycleScope.launchWhenResumed {  }

在这里,拿 launchWhenResumed 试验下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        println("onCreate 正在执行")

        lifecycleScope.launchWhenResumed {
            println("launchWhenResumed 正在执行")
        }
    }

    override fun onStart() {
        super.onStart()
        println("onStart 正在执行")
    }

    override fun onResume() {
        super.onResume()
        println("onResume 正在执行")
    }
}

输出结果:

I/System.out: onCreate 正在执行
I/System.out: onStart 正在执行
I/System.out: onResume 正在执行
I/System.out: launchWhenResumed 正在执行

ViewModelScope

LifecycleScope 在 AppCompatActivity 和 Fragment 中使用已经十分方便了,不过,目前很多项目都开始使用 MVVM,更多逻辑操作都是在 ViewModel 层,这时,就需要用到 ViewModelScope 了,它会自动绑定 ViewModel 的生命周期。

若需要使用 ViewModelScope,我们需要添加依赖:

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'

创建一个 ViewModel :

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            println("viewModelScope launch  ---  threadName: ${Thread.currentThread().name}")
            delay(5000)
            if (isActive){
                println("viewModelScope launch 执行完成")
            }
        }
    }
}

为了方便使用 viewModels() 去生成 ViewModel,还需要添加依赖:

    implementation "androidx.activity:activity-ktx:1.4.0"
    implementation "androidx.fragment:fragment-ktx:1.4.1"

在 Activity 中写测试代码:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        println("onCreate 正在执行")
        val model: MyViewModel by viewModels()
        model.toString()
    }

    override fun onDestroy() {
        super.onDestroy()
        println("onDestroy 正在执行")
    }
}

运行,当 Activity 启动后按返回键关闭 Activity,即可看到以下输出结果:

I/System.out: onCreate 正在执行
I/System.out: viewModelScope launch  ---  threadName: main
I/System.out: onDestroy 正在执行

由此也可以看出,viewModelScope 默认执行的线程也是主线程,当 Activity 和 Fragment 销毁后,也会自动取消协程。

当然,假如你想要手动取消协程,也可以通过这种方式:

viewModelScope.cancel()

LiveData

em...LiveData 跟上面所说的几种协程封装有些不太一样,大家都知道,LiveData 的核心功能并不是操控协程,所以,这里指的是 LiveData 与协程的一个协作。

首先,我们需要添加 LiveData 的依赖:

implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'

创建 LiveData:

    private val userName: LiveData<String> = liveData {
        val name = getName()
        emit(name)
    }
    
    private suspend fun getName(): String{
        delay(3000)
        println("设置名字 thread: ${Thread.currentThread().name}")
        return "不近视的猫"
    }

这里需要注意的是,getName() 是个挂起函数,并且我在里面模拟了耗时操作,并且拿到值后,通过 emit(name) 去修改值

对于 LiveData 进行监听:

        userName.observe(this){
            println("名字更新了: $it")
        }

运行结果:

I/System.out: 设置名字 thread: main
I/System.out: 名字更新了: 不近视的猫

LiveData 总结:

  • 可以通过 liveData{} 去创建 LiveData 对象,并且可以在里面执行挂起方法,在获取到值后,使用 emit() 去更新值。
  • 更新值的时候,是在主线程中运行。

其它补充

通过本文,相信大家对于 Kotlin 协程的结构化并发有着一定的理解了,但是,不知道大家有没有想过一个问题,那就是以上协程运行大多都是默认在主线程中,那有没有办法让其在其它线程执行?

答案当然是有的,而且也很简单,这里,我以 lifecycleScope 为例:

        lifecycleScope.launch(Dispatchers.IO) {
            println("1---thread: ${Thread.currentThread().name}")
            delay(5000)
            if (isActive){
                println("2---thread: ${Thread.currentThread().name}")
            }
        }

启动后,按返回键关闭页面:

I/System.out: 1---thread: DefaultDispatcher-worker-1