AsyncTask 作为 kotlin 协程

Posted

技术标签:

【中文标题】AsyncTask 作为 kotlin 协程【英文标题】:AsyncTask as kotlin coroutine 【发布时间】:2019-08-08 01:50:23 【问题描述】:

AsyncTask 的典型用途:我想在另一个线程中运行一个任务,完成该任务后,我想在我的 UI 线程中执行一些操作,即隐藏一个进度条。

任务将在TextureView.SurfaceTextureListener.onSurfaceTextureAvailable 开始,完成后我想隐藏进度条。同步执行此操作不起作用,因为它会阻止构建 UI 的线程,使屏幕变黑,甚至不显示我之后想要隐藏的进度条。

到目前为止,我使用这个:

inner class MyTask : AsyncTask<ProgressBar, Void, ProgressBar>() 
    override fun doInBackground(vararg params: ProgressBar?) : ProgressBar 
        // do async
        return params[0]!!
    

    override fun onPostExecute(result: ProgressBar?) 
        super.onPostExecute(result)
        result?.visibility = View.GONE
    

但是这些课程非常丑陋,所以我想摆脱它们。 我想用 kotlin 协程来做到这一点。我尝试了一些变体,但它们似乎都不起作用。我最有可能怀疑的工作是这样的:

runBlocking 
        // do async

progressBar.visibility = View.GONE

但这不能正常工作。据我了解,runBlocking 不会像AsyncTask 那样启动新线程,这就是我需要它做的事情。但是使用thread 协程,我看不到一种合理的方式来在它完成时得到通知。另外,我也不能把progressBar.visibility = View.GONE放到一个新线程中,因为只有UI线程可以做这样的操作。

我是协程的新手,所以我不太明白我在这里缺少什么。

【问题讨论】:

Benjamin Basmaci,您仍然认为接受的答案是正确的吗?也许您会重新考虑您的选择,因为这个问题似乎很受欢迎,而寻求答案的用户可能会感到困惑。 @Sergey 非常感谢您的回答和您的 cmets。我相信你帮助了很多人,它显然显示在赞成票中。我更改了接受的答案,但我没有选择您的任何一个答案。原因是,我特别想要一个苗条、快速的解决方案,它不涉及为我拥有的每个用例编写新类。出于这个原因,我做出了自己的回答并接受了这个答案,因为这就是我目前正在做的事情。不确定这两种方法的优缺点,但这就是我正在寻找的这个问题。 【参考方案1】:

要使用协程,您需要做以下几件事:

实现CoroutineScope接口。 对 JobCoroutineContext 实例的引用。 在调用在后台线程中运行代码的函数时,使用 suspend 函数修饰符挂起协程而不阻塞主线程。 使用 withContext(Dispatchers.IO) 函数在后台线程中运行代码并使用 launch 函数启动协程。

通常我为此使用一个单独的类,例如“Presenter”或“ViewModel”

class Presenter : CoroutineScope 
    private var job: Job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job // to run code in Main(UI) Thread

    // call this method to cancel a coroutine when you don't need it anymore,
    // e.g. when user closes the screen
    fun cancel() 
        job.cancel()
    

    fun execute() = launch 
        onPreExecute()
        val result = doInBackground() // runs in background thread without blocking the Main Thread
        onPostExecute(result)
    

    private suspend fun doInBackground(): String = withContext(Dispatchers.IO)  // to run code in Background Thread
        // do async work
        delay(1000) // simulate async work
        return@withContext "SomeResult"
    

    // Runs on the Main(UI) Thread
    private fun onPreExecute() 
        // show progress
    

    // Runs on the Main(UI) Thread
    private fun onPostExecute(result: String) 
        // hide progress
    

使用ViewModel,代码使用viewModelScope更简洁:

class MyViewModel : ViewModel() 
    
    fun execute() = viewModelScope.launch 
        onPreExecute()
        val result = doInBackground() // runs in background thread without blocking the Main Thread
        onPostExecute(result)
    

    private suspend fun doInBackground(): String = withContext(Dispatchers.IO)  // to run code in Background Thread
        // do async work
        delay(1000) // simulate async work
        return@withContext "SomeResult"
    

    // Runs on the Main(UI) Thread
    private fun onPreExecute() 
        // show progress
    

    // Runs on the Main(UI) Thread
    private fun onPostExecute(result: String) 
        // hide progress
    

要使用viewModelScope,请将下一行添加到应用的 build.gradle 文件的依赖项:

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION"

在撰写本文时final LIFECYCLE_VERSION = "2.3.0-alpha04"

【讨论】:

【参考方案2】:

另一种方法是在CoroutineScope上创建通用扩展函数:

fun <R> CoroutineScope.executeAsyncTask(
        onPreExecute: () -> Unit,
        doInBackground: () -> R,
        onPostExecute: (R) -> Unit
) = launch 
    onPreExecute()
    val result = withContext(Dispatchers.IO)  // runs in background thread without blocking the Main Thread
        doInBackground()
    
    onPostExecute(result)

现在我们可以将它与任何CoroutineScope 一起使用:

ViewModel:

  class MyViewModel : ViewModel() 

      fun someFun() 
          viewModelScope.executeAsyncTask(onPreExecute = 
              // ...
          , doInBackground = 
              // ...
              "Result" // send data to "onPostExecute"
          , onPostExecute = 
              // ... here "it" is a data returned from "doInBackground"
          )
      
  

ActivityFragment

  lifecycleScope.executeAsyncTask(onPreExecute = 
      // ...
  , doInBackground = 
      // ...
      "Result" // send data to "onPostExecute"
  , onPostExecute = 
      // ... here "it" is a data returned from "doInBackground"
  )

要使用viewModelScopelifecycleScope,将下一行添加到应用的build.gradle 文件的依赖项:

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION" // for viewModelScope
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$LIFECYCLE_VERSION" // for lifecycleScope

在撰写本文时final LIFECYCLE_VERSION = "2.3.0-alpha05"

【讨论】:

保持熟悉度的最佳解决方案。你可以为此添加一个java版本吗?我试图从 kotlin 反编译它,但生成的代码看起来很垃圾。 @chitgoks 这个实现是基于 Coroutines 的,Java 没有 Coroutines,所以在 Java 中很难做到。 我爱你!我想我只有在阅读了这篇 sn-p 之后才真正理解了协程【参考方案3】:

您可以让 ProgressBar 在 UI 主线程上运行,同时使用协程异步运行您的任务。

在你的重写 fun onCreate() 方法中,

GlobalScope.launch(Dispatchers.Main)  // Coroutine Dispatcher confined to Main UI Thread
    yourTask() // your task implementation

你可以初始化,

private var jobStart: Job? = null

在 Kotlin 中,var 声明意味着属性是可变的。如果你 将其声明为 val,它是不可变的、只读的且不能重新分配。

在 onCreate() 方法之外,yourTask() 可以实现为一个挂起函数,它不会阻塞主调用者线程。

当函数在等待返回结果的过程中被挂起时,它正在运行的线程被解除阻塞,以供其他函数执行。

private suspend fun yourTask() = withContext(Dispatchers.Default) // with a given coroutine context
    jobStart = launch 
       try
        // your task implementation
        catch (e: Exception) 
             throw RuntimeException("To catch any exception thrown for yourTask", e)
      
    
  

对于您的进度条,您可以创建一个按钮以在单击按钮时显示进度条。

buttonRecognize!!.setOnClickListener 
    trackProgress(false)

在 onCreate() 之外,

private fun trackProgress(isCompleted:Boolean) 
    buttonRecognize?.isEnabled = isCompleted // ?. safe call
    buttonRecognize!!.isEnabled // !! non-null asserted call

    if(isCompleted) 
        loading_progress_bar.visibility = View.GONE
     else 
        loading_progress_bar.visibility = View.VISIBLE
    

另一个提示是检查您的协程是否确实在运行 另一个线程,例如。 DefaultDispatcher-worker-1,

Log.e("yourTask", "Running on thread $Thread.currentThread().name")

希望这有帮助。

【讨论】:

【参考方案4】:

首先,您必须使用launch(context) 运行协程,而不是使用runBlocking: https://kotlinlang.org/docs/reference/coroutines/coroutine-context-and-dispatchers.html

其次,要得到onPostExecute的效果,必须使用

Activity.runOnUiThread(Runnable) 或View.post(Runnable)。

【讨论】:

没有必要使用Activity.runOnUiThread(Runnable) or View.post(Runnable).。使用适当的Dispatchers,您可以在 UI 线程中调用函数。看我的回答***.com/a/58900195/1731626【参考方案5】:

这不使用协程,但它是让任务在后台运行并在之后在 UI 上执行某些操作的快速解决方案。

我不确定这种方法与其他方法相比的优缺点,但它很有效并且非常容易理解:

Thread 
    // do the async Stuff
    runOnUIThread 
        // do the UI stuff
    
    // maybe do some more stuff
.start()

使用此解决方案,您可以轻松地在两个实体之间传递值和对象。你也可以无限嵌套。

【讨论】:

这不是一个好的解决方案。你可能有后台任务执行很长时间,同时你的活动可能会被破坏,调用runOnUiThread 方法时可能会出现异常。顺便说一句,这个解决方案没有使用协程。 @Sergey 虽然我不同意这种方法的风险推理,但我回顾了问题和答案。虽然问题是针对像我这样的解决方案,但现在就是这样写的。原因是我当时缺乏对 kotlin、android 和协程的理解。这种缺乏经验导致我专门要求协程。正如你所说,这个问题很受欢迎,我的解决方案不涉及协程。我最初的意图和问题的确切措辞非常不同。然而,措辞才是最重要的。所以我将重新分配接受的解决方案。谢谢!【参考方案6】:

以下方法可能能够满足您的需求。它需要更少的样板代码,并且适用于 100% 的用例

GlobalScope.launch 
                bitmap = BitmapFactory.decodeStream(url.openStream())

            .invokeOnCompletion 
                createNotification()
            

【讨论】:

不建议使用GlobalScope。来自文档:“...在 GlobalScope 的实例上使用异步或启动是非常不鼓励的。”

以上是关于AsyncTask 作为 kotlin 协程的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin协程概览

Kotlin协程概览

Kotlin 协程 - 延迟,它是如何工作的?

Kotlin协程的迷惑

1.协程-为何要用协程

深入理解Kotlin协程协程作用域启动模式调度器异常和取消使用篇