在后台线程执行硬任务,在主线程返回结果

Posted

技术标签:

【中文标题】在后台线程执行硬任务,在主线程返回结果【英文标题】:Execute hard task in Background Thread, return result in Main Thread 【发布时间】:2019-08-18 05:39:33 【问题描述】:

我花了一些时间寻找一个开发人员友好的解决方案(不向项目添加依赖项)如何在后台线程中执行一些硬任务,并在任务完成后将结果返回到主线程。我找到了允许这样做的“AsyncTask”。但是要使用它,您需要为需要在后台运行的每个任务编写样板代码。我是 ios 开发者,决定尝试 android 相关的开发。所以在 Swift 中你可以简单地使用下面的代码来完成这个任务:

DispatchQueue.global().async(execute: 
      //Do some hard task in background
   DispatchQueue.main.async(execute: 
      //Return to main
   )
)

这看起来很简单。但是在 Kotlin 中我没有找到这么简单的解决方案,于是决定创建它。

这是我做的:

我创建了通用类

import android.os.AsyncTask

class BaseAsyncTask<M>: AsyncTask<()->M, Int, M>() 

    var completion: ((M)->Unit)? = null

    override fun doInBackground(vararg params: (() -> M)?): M? 
        for (p in params) 
            return p?.invoke()
        
        return  null
    

    override fun onPostExecute(result: M) 
        super.onPostExecute(result)

        completion?.invoke(result)
    

和经理

class AsyncManager 

    companion object 

        fun <M>execute(inBackground: ()->M, inMain: (M)->Unit): BaseAsyncTask<M> 
            val task = BaseAsyncTask<M>()
            task.completion = inMain
            task.execute(inBackground)

            return task
        

        fun <M>execute(inBackground: ()->M): BaseAsyncTask<M> 
            val task = BaseAsyncTask<M>()
            task.execute(inBackground)

            return task
        
    


现在我这样使用它:

AsyncManager.execute(
   //Do some hard task in background
, 
  //Return to main
)

看起来对开发者友好。

Log.e("MAIN", "MAIN THREAD SHOULD NOT BE BLOCKED")

AsyncManager.execute(
    Log.e("TASK", "Started background task")
    val retval = "The value from background"
    Thread.sleep(5000)
    Log.e("TASK", "Finished background task with result: " + retval)
    retval
, 
    Log.e("TASK", "Started task in Main thread with result from Background: " + it)
)

Log.e("MAIN", "MAIN THREAD SHOULD NOT BE BLOCKED - 1")

还有日志:

2019-03-27 17:11:00.719 17082-1​​7082/com.test.testapp E/MAIN: MAIN 线程不应该被阻塞

2019-03-27 17:11:00.722 17082-1​​7082/com.test.testapp E/MAIN: MAIN 线程不应该被阻塞 - 1

2019-03-27 17:11:00.722 17082-1​​7124/com.test.testapp E/TASK: 开始 后台任务

2019-03-27 17:11:05.737 17082-1​​7124/com.test.testapp E/TASK: 完成 具有结果的后台任务:来自后台的值

2019-03-27 17:11:05.738 17082-1​​7082/com.test.testapp E/TASK: 开始 主线程中的任务,结果来自背景:值来自 背景

所以问题是专业的 Android 开发人员如何看待这个解决方案。如果我会使用它,我会遇到什么问题。也许有一些理由不使用这个解决方案。

【问题讨论】:

【参考方案1】:

如果您使用 Kotlin,正确的方法是通过 Coroutines,这样您就可以编写如下代码:

// Launch a coroutine that by default goes to the main thread
GlobalScope.launch(Dispatchers.Main) 
    // Switch to a background (IO) thread
    val retval = withContext(Dispatchers.IO) 
        Log.e("TASK", "Started background task")
        val retval = "The value from background"
        Thread.sleep(5000)
        Log.e("TASK", "Finished background task with result: " + retval)
        retval
    
    // Now you're back the main thread
    Log.e("TASK", "Started task in Main thread with result from Background: " + retval)

请注意,Kotlin 协程在 structured concurrency 下运行,因此您通常希望避免使用 GlobalScope,而是将您的协程范围绑定到您的 Activity / Fragment 生命周期。这通常需要您自己立即完成。

【讨论】:

感谢您的回答。我一直在寻找解决方案而不向项目添加依赖项。 @Dmitry - 我认为你需要克服这种心理模型。为已经存在 100 倍更好和更好测试的东西编写一个更糟糕的版本绝不是一种可持续的方法。 谢谢。我会考虑的。 与回答中提到的一样,Kotlin 协程可以由有助于管理协程何时运行的范围来定义。最有用的范围之一是 ViewModelScope,它为您的应用程序中的每个 ViewModel 定义.了解更多信息developer.android.com/topic/libraries/architecture/…【参考方案2】:

ianhanniballake's answer 是正确的,但可能有点不完整,所以我想我会提供一个完整的通用示例。

build.gradle(:app):

dependencies  // this line is probably already present
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"

全局 CoroutineScope 不绑定到任何作业。 使用 GlobalScope 启动整体运行的***协程 应用程序生命周期,并且不会提前取消。应用 代码通常应该使用应用程序定义的 CoroutineScope。 使用 强烈建议不要在 GlobalScope 实例上进行异步或启动取自here

所以你想使用任何生命周期为CoroutineScope 的类,这样当它死亡时,它会将运行的后台任务带入坟墓。通常,人们建议为此使用活动。但是,有一个case to be made,您不希望任何外部类将您的活动用作他们的CoroutineScope,因此您可以使用受保护的字段:

protected val scope = CoroutineScope(Job() + Dispatchers.Main)

在撰写本文时,我不知道为什么我们必须在这里创建一个Job()。我所知道的是 + 运算符被重载以将这两个上下文合并为一个。对于Dispatcher部分,你可以选择一个合理的。选项包括

Dispatchers.Main 用于 UI 线程 Dispatchers.Default 用于后台线程池 Dispatchers.IO 用于阻塞 I/O 密集型操作 Dispatchers.Unconfined 当您真正知道自己在做什么时。 This article 不鼓励“正常”使用它。

现在所有这些都解决了,代码变得非常简单:

import kotlin.coroutines.*
// ...
myButton.setOnClickListener()  v: View? ->
                myButton.setColorToYellow() // some UI thread work
                scope.launch(Dispatchers.Default) 
                    val result = longComputation() // some background work

                    withContext(Dispatchers.Main) 
                        // some UI thread work for when the background work is done
                        root.findViewById<TextView>(R.id.text_home).text = "Result: $result"
                    
                
                myButton.setColorToRed() // more UI thread work. this is done instantly
       

当然,这可以在任何地方完成 - 我只是使用一个按钮和一个 onClickListener 来举例说明一个可能的用例。

【讨论】:

以上是关于在后台线程执行硬任务,在主线程返回结果的主要内容,如果未能解决你的问题,请参考以下文章

Realm - 在后台线程上批量更新 RLMResults

我们可以在后台队列中生成 Realm 结果并在主线程上使用它吗

如何在后台线程上创建 NSTimer?

如何在后台队列中获取对象的领域结果,然后在主线程上使用它

Magical Record - 在主线程块 ui 中获取,在后台返回 nil

从另一个线程在主线程中运行代码