util 类中的自定义 CoroutineScope

问题描述 投票:0回答:1

我有

PrinterUtil
类,我认为它会导致内存泄漏并想重构它,但我不确定协程的新实现是否正确

PrintUtil
有方法
printImage
接受 listImage,然后 listImage 被循环,并且对于每个循环,它将创建新的 coroutineScope 我认为最好创建
coroutineScope
的全局变量并在循环内调用
async
,而不是在循环内创建新的协程

  1. 旧方法真的会导致内存泄漏吗?
  2. 新方法是否正确并且会相应地得到清除?

老方法

  Object PrintUtil{
    fun printImage(listImage : List<Image>) = CoroutineScope(Dispatchers.Main).launch {
        listImage.foreach{
          val isSuccess = async{try{
              //call suspend method of printing process, throw error if problem occur
            }catch(e : Exception){Log.e("error printing",e.message); false}
          }.await() 
        }
    }
  }

新方法

Object PrintUtil{
  val job = Job()
  val scope = CoroutineScope(Dispatcher.IO + job)

  fun printImage(listImage : List<Image>){
    listImage.foreach{
      val isSuccess = scope.async{ 
        try{
          //call suspend method of printing process, throw error if problem occur
        }catche(e : Exception){
          Log.e("error printing",e.message)
           false
        }
      }.await()
    }      
  }

  //execute on activity onDestroy
  fun cancel(){
    job.cancel()
  }

}
android kotlin memory-leaks singleton coroutine
1个回答
0
投票

首先,我们假设您所描述的

suspend method of printing process
看起来像这样:

suspend fun printProcess(image: Image) 

内存泄漏是指随着时间的推移积累过时的对象,而这些对象永远不会从内存中删除,这在任何一个变体中都不会发生。可能发生的最糟糕的事情是启动的协程不会被取消,并将继续运行直到完成。在那之前它们仍然会占用内存,并且所有引用的对象都不会被垃圾收集清理掉。这可能是

Image
对象的相关因素:它们仍将保留在内存中,直到所有协程完成。这还包括
Image
对象可能引用的所有其他对象。这取决于
Image
实际上是什么。由于所有这些都会被清理最终,从技术上讲这不是内存泄漏。但这可能是不受欢迎的行为。

因此取消协程是防止这种情况的好方法。但是,使用

job.cancel()
不仅会取消协程,还会取消范围,因为您使用该作业创建了协程作用域。这意味着对
printImage
的所有进一步调用都不会执行任何操作,因为在该范围内无法再执行任何操作。您选择在 Activity 的
cancel
中调用
onDestroy
,但如果您的 Activity 被重新创建(通常发生在仅通过旋转更改设备配置时发生的情况),
PrintUtil
对象仍会在那里,但不会出现”不再工作了。要解决此问题,您应该调用
job.cancelChildren()
。这只会取消在作用域中启动的任何协程,而不是作用域本身。对
printImage
的任何进一步调用都将按预期工作。

您实际上也可以致电

scope.coroutineContext.cancelChildren()
,这样您就不需要
job
。然后可以完全删除该变量及其用法。

像这样取消所有协程只是第一步。由于在 Kotlin 中取消协程是合作性的,因此要使其产生任何效果,

printProcess
必须满足此类请求。例如,它可以以适当的时间间隔调用
yield
。如果它启动任何新的协程,他们应该使用
ensureActive()
(任何协程都应该希望满足取消请求)。

话虽如此,我认为您应该首先重新考虑将

PrintUtil
制作为全局对象。您已经确定了对某个活动的依赖性,但实际上您将取消 PrintUtil 中的
all
协程,而不仅仅是那些从正在被销毁的活动启动的协程。您目前可能只有一项活动,因此这并不重要,但最好将对一项活动的依赖形式化。这样你也不会忘记在
cancel
中调用
onDestroy
。我建议如下:

class ImagePrinter(lifecycleScope: CoroutineScope) {
    private val scope = lifecycleScope + Dispatchers.IO

    operator fun invoke(listImage: List<Image>) {
        listImage.forEach {
            // use scope here
        }
    }
}

然后你可以在每个活动中创建一个对象,如下所示:

val printImage = ImagePrinter(lifecycleScope)

lifecycleScope
是一个CoroutineScope,当activity被销毁时会自动取消,所以不需要
cancel
方法。确保使用 gradle 依赖项
androidx.lifecycle:lifecycle-runtime-ktx
,以便可以访问
lifecycleScope

您可能还注意到该函数现在声明为

operator fun invoke(...)
。这简化了对象的使用。如果您想打印图像,只需调用:

printImage(listOfImages)

如果您想在其他函数中使用它,只需提供

printImage::invoke
作为类型
(List<Image>) -> Unit
的参数。

关于如何启动新协程的最后几句话:虽然不是很清楚,但我认为您想要实现的是

printImage
应立即返回,因此连续的调用应同时并发执行。然而,对于任何给定的图像列表,您希望顺序迭代该列表,以便仅在第一个图像的
printProcess
结束时才处理第二个图像。然后你应该将循环移到协程内:

scope.launch {
    listImage.forEach {
        ensureActive()
        val isSuccess = try {
            printProcess(it)
        } catch (e: Exception) {
            Log.e("error printing", "${e.message}")
            false
        }
    }
}

正如 @broot 在评论中已经提到的那样,永远不要使用

async
启动新的协程并立即
await
它。这是没有意义的,可以直接调用挂起函数,如上所示。

© www.soinside.com 2019 - 2024. All rights reserved.