写点什么

史上最详 Android 版 kotlin 协程入门进阶实战 (四),架构师必备

用户头像
Android架构
关注
发布于: 刚刚

"请求结果"


}


btn.text = result


launch {


//网络请求 3...


}


}


}


  • 因为我们的GlobalScope默认使用的是Dispatchers.Default,这会导致我们在非主线程上刷新 UI。

  • 子协程产生异常会产生相互干扰。子协程异常取消会导致父协程取消,同时其他子协程也将会被取消。

  • 如果我们这个时候activity或者framgent退出,因为协程是在GlobalScope中运行,所以即使activity或者framgent退出,这个协程还是在运行,这个时候会产生各种泄露问题。同时此协程当执行到刷新操作时,因为我们的界面已经销毁,这个时候执行 UI 刷新将会产生崩溃。


如果我们要解决上面的问题。我们得这么做:


var job:Job? = null


private fun start() {


job = GlobalScope.launch(Dispatchers.Main + SupervisorJob()) {


launch {


throw NullPointerException("空指针")


}


val result = withContext(Dispatchers.IO) {


//网络请求...


"请求结果"


}


launch {


//网络请求 3...


}


btn.text = result


}


}


override fun onDestroy() {


super.onDestroy()


job?.cancel()


}


我们先需要通过launch启动时加入Dispatchers.Main来保证我们是在主线程刷新 UI,同时还需要再GlobalScope.launch的协程上下文中加入SupervisorJob来避免子协程的异常取消会导致整个协程树被终结。 最后我们还得把每次通过GlobalScope启动的Job保存下来,在activity或者framgent退出时调用job.cancel取消整个协程树。这么来一遍感觉还行,但是我们不是写一次啊,每次写的时候会不会感觉超麻烦,甚至怀疑人生。



所以官方在 kotlin 协程中提供了一个默认在主线程运行的协程:MainScope,我们可以通过它来启动协。


public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)


我们可以看到MainScope的创建默认就使用了SupervisorJobDispatchers.Main。说明我们可以通过MainScope来处理 UI 组件刷新。同时由于MainScope采用的是SupervisorJob,所以我们各个子协程中的异常导致的取消操作并不会导致MainScope的取消。这就很好的简化了我们通过GlobalScope去启动一个协程的过程。


private val mainScope = MainScope()


private fun start() {


mainScope.launch {


launch {


throw NullPointerException("空指针")


}


val result = withContext(Dispatchers.IO) {


//网络请求...


"请求结果"


}


launch {


//网络请求 3...


}


btn.text = result


}


}


override fun onDestroy() {


super.onDestroy()


mainScope.cancel()


}


通过使用MainScope我们是不是省略了很多操作。同时我们也不需要保存每一个通过MainScope启动的Job了,直接在最后销毁的时候调用mainScope.cancel()就能取消所有通过mainScope启动的协程。


这里多提一点:可能这里有的人会想,我使用GlobalScope也不保存启动的Job,直接GlobalScope.cancel不行吗?如果是这样的话,那么恭喜你喜提超级崩溃 BUG 一个。这里就不扩展了。可以自己动手去试试,毕竟实践出真理。


那可能还有人想,我连创建MainScope都懒得写,而且脑子经常不好使,容易忘记调用mainScope进行cancel操作怎么办。



官方早就为我们这些懒人想好了解决方案,这个时候我们只需要再集成一个 ktx 运行库就可以了。


在 Activity 与 Framgent 中使用协程




implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"


这个时候我们就可以在activity或者framgent直接使用lifecycleScope进行启动协程。我们看来看看 activity 中的lifecycleScope实现


public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope


get() = lifecycle.coroutineScope


我们可以到lifecycleScope它是通过lifecycle得到一个coroutineScope,是一个LifecycleCoroutineScope对象。


public val Lifecycle.coroutineScope: LifecycleCoroutineScope


get() {


while (true) {


val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?


if (existing != null) {


return existing


}


val newScope = LifecycleCoroutineScopeImpl(


this,


SupervisorJob() + Dispatchers.Main.immediate


)


if (mInternalScopeRef.compareAndSet(null, newScope)) {


newScope.register()


return newScope


}


}


}


我们可以看到lifecycleScope采用的和MainScope一样的创建CoroutineScope,同时它又通过结合lifecycle来实现当lifecycle状态处于DESTROYED状态的时候自动关闭所有的协程。


public abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {


internal abstract val lifecycle: Lifecycle


public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {


lifecycle.whenCreated(block)


}


public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {


lifecycle.whenStarted(block)


}


public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {


lifecycle.whenResumed(block)


}


}


internal class LifecycleCoroutineScopeImpl(


override val lifecycle: Lifecycle,


override val coroutineContext: CoroutineContext


) : LifecycleCoroutineScope(), LifecycleEventObserver {


init {


if (lifecycle.currentState == Lifecycle.State.DESTROYED) {


coroutineContext.cancel()


}


}


fun register() {


launch(Dispatchers.Main.immediate) {


if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {


lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)


} else {


coroutineContext.cancel()


}


}


}


override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {


if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {


lifecycle.removeObserver(this)


coroutineContext.cancel()


}


}


}


同时我们也可以通过launchWhenCreatedlaunchWhenStartedlaunchWhenResumed来启动协程,等到lifecycle处于对应状态时自动触发此处创建的协程。


比如我们可以这么操作:


class MainTestActivity : AppCompatActivity() {


init {


lifecycleScope.launchWhenResumed {


Log.d("init", "在类初始化位置启动协程")


}


}


override fun onCreate(savedInstanceState: Bundle?) {


super.onCreate(savedInstanceState)


setContentView(R.layout.activity_main)


}


}


D/onResume: onResume


D/init: 在类初始化位置启动协程


按照我们正常情况加载顺序,是不是应该init先执行输出?然而在实际情况中它是在等待Activity进入onResume状态以后才执行接着看launchWhenResumed中调用的whenResumed实现。


public suspend fun <T> Lifecycle.whenResumed(block: suspend CoroutineScope.() -> T): T {


return whenStateAtLeast(Lifecycle.State.RESUMED, block)


}


public suspend fun <T> Lifecycle.whenStateAtLeast(


minState: Lifecycle.State,


block: suspend CoroutineScope.() -> T


): T = withContext(Dispatchers.Main.immediate) {


val job = coroutineContext[Job] ?: error("when[State] methods should have a parent job")


val dispatcher = PausingDispatcher()


val controller =


LifecycleController(this@whenStateAtLeast, minState, dispatcher.dispatchQueue, job)


try {


withContext(dispatcher, block)


} finally {


controller.finish()


}


}


@MainThread


internal class LifecycleController(


private val lifecycle: Lifecycle,


private val minState: Lifecycle.State,


private val dispatchQueue: DispatchQueue,


parentJob: Job


) {


private val observer = LifecycleEventObserver { source, _ ->


if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {


handleDestroy(parentJob)


} else if (source.lifecycle.currentState < minState) {


dispatchQueue.pause()


} else {


dispatchQueue.resume()


}


}


init {


if (lifecycle.currentState == Lifecycle.State.DESTROYED) {


handleDestroy(parentJob)


} else {


lifecycle.addObserver(observer)


}


}


private inline fun handleDestroy(parentJob: Job) {


parentJob.cancel()


finish()


}


@MainThread


fun finish() {


lifecycle.removeObserver(observer)


dispatchQueue.finish()


}


}


我们可以看到,实际上是调用了whenStateAtLeast,同时使用了withContext进行了一个同步操作。然后在LifecycleController中通过添加LifecycleObserver来监听状态,通过lifecycle当前状态来对比我们设定的触发状态,最终决定是否恢复执行。


现在我们对于Activity中的lifecycleScope的创建以及销毁流程有了一个大概的了解。同理Fragment中的lifecycleScope实现原理也是和Activity是一样的,这里我们就不再重复讲解。我们做个简单的实验:


class MainActivity : AppCompatActivity() {


override fun onCreate(savedInstanceState: Bundle?) {


super.onCreate(savedInstanceState)


setContentView(R.layout.activity_main)


lifecycleScope.launch {


delay(2000)


Toast.makeText(this@MainActivity,"haha",Toast.LENGTH_SHORT).show()


}


}


}


这个时候是不是比之前的使用方式简单多了,我们既不用关心创建过程,也不用关心销毁的过程。


这个时候我们就需要提到CoroutineExceptionHandler协程异常处理。通过之前的章节我们知道,启动一个协程以后,如果未在协程上下文中添加CoroutineExceptionHandler情况下,一旦产生了未捕获的异常,那么我们的程序将会崩溃退出。


class MainActivity : AppCompatActivity() {


val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->


Log.d("exceptionHandler", "throwable")


}


fun load() {


lifecycleScope.launch(exceptionHandler) {


//省略...


}


lifecycleScope.launch(exceptionHandler) {


//省略...


}


lifecycleScope.launch(exceptionHandler) {


//省略...


}


}


}


当出现这种情况的时候,像笔者这种有严重偷懒情结的人就开始抓狂了。为什么要写这么多遍 lifecycleScope.launch,同时每一次启动都要手动添加CoroutineExceptionHandler。难道就不能再简便一点吗?



当然可以,首先我们自定义一个异常处理,,我们在实现上只做一个简单的异常日志输出:


/**


  • @param errCode 错误码

  • @param errMsg 简要错误信息

  • @param report 是否需要上报


*/


class GlobalCoroutineExceptionHandler(private val errCode: Int, private val errMsg: String = "", private val report: Boolean = false) : CoroutineExceptionHandler {


override val key: CoroutineContext.Key<*>


get() = CoroutineExceptionHandler


override fun handleException(context: CoroutineContext, exception: Throwable) {


val msg = exception.stackTraceToString()


Log.e("{msg}")


}


}


然后我们在通过 kotlin 的扩展函数来简化我们的使用,去掉重复写lifecycleScope.launchexceptionHandler的过程,我们就定义三个常用方法。


inline fun AppCompatActivity.requestMain(


errCode: Int = -1, errMsg: String = "", report: Boolean = false,


crossinline block: suspend CoroutineScope.() -> Unit) {


lifecycleScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {


block.invoke(this)


}


}


inline fun AppCompatActivity.requestIO(


errCode: Int = -1, errMsg: String = "", report: Boolean = false,


crossinline block: suspend CoroutineScope.() -> Unit): Job {


return lifecycleScope.launch(Dispatchers.IO + GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {


block.invoke(this)


}


}


inline fun AppCompatActivity.delayMain(


errCode: Int = -1, errMsg: String = "", report: Boolean = false,


delayTime: Long, crossinline block: suspend CoroutineScope.() -> Unit) {


lifecycleScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {


withContext(Dispatchers.IO) {


delay(delayTime)


}


block.invoke(this)


}


}


这个时候我们就可以愉快的在Activity中使用了


class MainActivity : AppCompatActivity() {


override fun onCreate(savedInstanceState: Bundle?) {


super.onCreate(savedInstanceState)


setContentView


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


(R.layout.activity_main)


requestMain {


delay(2000)


Toast.makeText(this@MainActivity,"haha",Toast.LENGTH_SHORT).show()


}


requestIO {


loadNetData()


}


delayMain(100){


Toast.makeText(this@MainActivity,"haha",Toast.LENGTH_SHORT).show()


}


}


private suspend fun loadNetData(){


//网络加载


}


}


同样的我们再扩展一套基于Fragment的方法


inline fun Fragment.requestMain(


errCode: Int = -1, errMsg: String = "", report: Boolean = false,


crossinline block: suspend CoroutineScope.() -> Unit) {


lifecycleScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {


block.invoke(this)


}


}


inline fun Fragment.requestIO(


errCode: Int = -1, errMsg: String = "", report: Boolean = false,


crossinline block: suspend CoroutineScope.() -> Unit) {


lifecycleScope.launch(Dispatchers.IO + GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {


block.invoke(this)


}


}


inline fun Fragment.delayMain(


errCode: Int = -1, errMsg: String = "", report: Boolean = false, delayTime: Long,


crossinline block: suspend CoroutineScope.() -> Unit) {


lifecycleScope.launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {


withContext(Dispatchers.IO) {


delay(delayTime)


}


block.invoke(this)


}


}


然后也可以愉快的在Fragment中使用了


class HomeFragment:Fragment() {


init {


lifecycleScope.launchWhenCreated {


Toast.makeText(context,"Fragment 创建了", Toast.LENGTH_SHORT).show()


}


}


override fun onCreateView(


inflater: LayoutInflater,


container: ViewGroup?,


savedInstanceState: Bundle?


): View? {


return inflater.inflate(R.layout.fragment_main,container,false)


}


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {


super.onViewCreated(view, savedInstanceState)


requestMain {


//...


}


requestIO {


//...


}


delayMain(100){


//...


}


}


}


这里需要提一下,可能有的人不太明白,为什么要把ActivityFragment都分开写,他们都是使用的lifecycleScope,我们直接通过lifecycleScope扩展就不可以了吗。假如我们这么扩展:


inline fun LifecycleCoroutineScope.requestMain(


errCode: Int = -1, errMsg: String = "", report: Boolean = false,


crossinline block: suspend CoroutineScope.() -> Unit) {


launch(GlobalCoroutineExceptionHandler(errCode, errMsg, report)) {


block.invoke(this)


}


}


复制代码


我们以Dailog为例,来启动一个协程:


val dialog = Dialog(this)


dialog.show()


(dialog.context as LifecycleOwner).lifecycleScope.requestMain {


withContext(Dispatchers.IO){


//网络加载


}


// 刷新 UI


}


dialog.cancel()


那么可能会出现一个什么问题?是的,内存泄露的问题以及错误的引用问题。虽然我的dialog被销毁了,但是我们lifecycleScope并不处于DESTROYED状态,所以我们的协程依然会执行,这个时候我们就会出现内存泄露和崩溃问题。


通过上面的学习,我们已经基本掌握了协程在ActivityFragment中的使用方式。接下来我们讲解在Viewmodel中使用协程。


ViewModel 中使用协程




如果我们想和在ActivityFragment中一样的简便、快速的在ViewModel使用协程。那么我们就需要集成下面这个官方的ViewModel扩展库。


implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"


ActivityFragment不同的是,在ViewModel我们使用的不是lifecycleScope,而是使用viewModelScope,使用viewModelScope,使用viewModelScope。重要的事情说三遍。


这里一定要注意噢,之前就有好几个人问我为什么在viewmodel里面用不了协程,我开始纳闷半天咋就用不了呢。最后一问结果是_在ViewModel使用lifecycleScope_,这样做是不对滴


public val ViewModel.viewModelScope: CoroutineScope


get() {


val scope: CoroutineScope? = this.getTag(JOB_KEY)


if (scope != null) {


return scope


}


return setTagIfAbsent(


JOB_KEY,


CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)


)


}


internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {


override val coroutineContext: CoroutineContext = context


override fun close() {


coroutineContext.cancel()


}


}


viewModelScope相比较lifecycleScope实现会稍微简单一点。都是使用的SupervisorJob() + Dispatchers.Main上下文,同时最终的取消操作也类似lifecycleScope,只不过viewModelScope取消是在ViewModel的销毁的时候取消。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
史上最详Android版kotlin协程入门进阶实战(四),架构师必备