将后台线程的结果传达给 Android 中的 Ui 线程的正确方法

Posted

技术标签:

【中文标题】将后台线程的结果传达给 Android 中的 Ui 线程的正确方法【英文标题】:Correct way to communicate the result of a background thread to the Ui Thread in Android 【发布时间】:2021-03-08 02:33:49 【问题描述】:

这对我来说是最令人困惑的话题之一。 所以我的问题是,当这完成时,传达后台线程结果的正确方法是什么?

假设我想用我刚刚下载的一些信息更新一些TextView。当我需要执行后台任务时,我会使用 3 件事:

异步任务

非常好用,这个有onPostExecute() 方法,可以将结果直接返回到UiThread,所以我可以使用回调接口或做任何我想做的事情。我喜欢这门课,但它已弃用。

线程池执行器

这是我在需要执行后台任务时实际使用的,我的问题来了,我必须将结果提供给 UiThread。我已经了解了LooperHandler 课程以及mainLooper

所以,当我需要返回一些结果时,我使用 runOnUiThread() 方法,正如我所读的那样,只需获取 Ui 线程的 Looper 并将我的 Runnable 发布到队列中。

这很好用,我可以与主线程通信,但我觉得它真的很难看,而且我确信有一种比填充我所有的“runOnUiThread()”方法代码更优雅的方法。另外,如果后台任务需要太多时间,当runOnUiThread() 中的代码运行时,用户可能已经更改了ActivityFragment,这将导致Exceptions(我知道使用LiveDataMVVM 模式将解决最后一个问题,但我在一个遗留项目中工作,我无法重构所有代码,所以我正在使用经典的 Activity mvc 模式)

那么,还有另一种方法吗?你能举个例子吗?我确实搜索了很多但没有找到任何东西......

协程

我实际上在一个遗留项目中工作,我必须使用 Java,所以不能使用 Kotlin coroutines,但我发现它们易于使用且功能强大。

任何帮助将不胜感激!

【问题讨论】:

这是唯一的方法吗? @blackapps 你读过OP吗?我搜索了它,我根据项目使用了几种替代方案,但我从未找到将线程结果与主线程通信的“正确”方式。例如,我知道“mvvm”模式是正确的结构方法,因为谷歌在文档中说它,但我没有发现任何东西作为 Ui 线程和其他线程之间通信的“正确方式”。我知道 Java 中使用的同步和线程安全共享对象的概念,但我想知道是否有一种“android”方式来进行这种通信。 我认为判断什么是“正确”的方式有点主观,但查看 AOSP 源代码我可以看到与runOnUiThread 非常相似的模式。他们创建一个新的Handler,并在每次后台操作之后,向负责更新 UI 的 Handler 发布一个新的 runnable。 使用可以尝试使用RXJava。 【参考方案1】:

背景

在Android中,当应用程序启动时,系统会为应用程序创建一个执行线程,称为主线程(也称为UI线程)。 Google 介绍了主线程及其负责人如下。

主线程有一个非常简单的设计:它唯一的工作就是接受和 从线程安全的工作队列中执行工作块,直到它的应用程序 终止。该框架生成其中一些工作块 各种各样的地方。这些地方包括与 生命周期信息、用户事件(如输入)或即将到来的事件 从其他应用程序和进程。另外,app可以显式入队 自己的块,不使用框架。

您的应用执行的几乎所有代码块都与事件相关联 回调,例如输入、布局膨胀或绘制。当某事 触发事件,事件发生的线程推送 事件本身,并进入主线程的消息队列。这 然后主线程可以为事件提供服务。

当动画或屏幕更新发生时,系统会尝试 执行一个工作块(负责绘制屏幕) 每 16 毫秒左右,以便以每秒 60 帧的速度平滑渲染。 为了使系统达到这个目标,UI/View 层次结构必须更新 在主线程上。但是,当主线程的消息队列 包含对主要任务来说太多或太长的任务 线程以足够快的速度完成更新,应用程序应该移动它 工作到工作线程。如果主线程无法完成执行 在 16 毫秒内完成工作块,用户可能会观察到搭便车、滞后或 缺乏对输入的 UI 响应。如果主线程阻塞 大约 5 秒,系统显示 Application Not 响应 (ANR) 对话框,允许用户直接关闭应用程序。

要更新视图,必须在主线程中进行,如果尝试在后台线程中更新,系统会抛出CalledFromWrongThreadException

如何从后台线程更新主线程上的视图?

主线程分配有一个Looper 和一个MessageQueue。要更新视图,我们需要创建一个任务,然后将其放入 MessageQueue。为此,Android 提供 Handler API 允许我们将任务发送到主线程的 MessageQueue 以便稍后执行。

// Create a handler that associated with Looper of the main thread
Handler mainHandler = new Handler(Looper.getMainLooper());

// Send a task to the MessageQueue of the main thread
mainHandler.post(new Runnable() 
    @Override
    public void run() 
        // Code will be executed on the main thread
    
);

为了帮助开发人员轻松地从后台线程与主线程进行通信,Android 提供了几种方法:

Activity.runOnUiThread(Runnable)

View.post(Runnable)

View.postDelayed(Runnable, long)

在后台,他们使用 Handler API 来完成他们的工作。

回到你的问题

AsyncTask

这是一个设计为围绕Thread 和Handler 的辅助类的类。它负责:

创建一个线程或线程池以在后台执行任务

创建一个与主线程关联的Handler,将任务发送到主线程的MessageQueue。

它已从 API 级别 30 弃用

ThreadPoolExecutor

在 Java 中创建和处理线程有时很困难,如果开发人员处理不当,可能会导致很多错误。 Java 提供了 ThreadPoolExecutor 来更有效地创建和管理线程。

此 API 不提供任何更新 UI 的方法。

Kotlin Coroutines

Coroutines 是一种在 Android 上进行异步编程的解决方案,用于简化异步执行的代码。但它仅适用于 Kotlin。

所以我的问题是,传达结果的正确方式是什么 后台线程什么时候完成?。

1.使用Handler或基于Handler构建的机制

1.1.如果线程与Activity/Fragment绑定:

Activity.runOnUiThread(Runnable)

1.2.如果一个线程有一个视图的引用,比如Adapter类。

View.post(Runnable)

View.postDelayed(Runnable, long)

1.3.如果线程没有绑定到任何UI元素,那么你自己创建一个Handler。

Handler mainHandler = new Handler(Looper.getMainLooper);

注意: 使用 Handler 的一个好处是您可以使用它在线程之间进行 2 种方式的通信。这意味着您可以从后台线程向主线程的 MessageQueue 发送任务,而从主线程,您可以向后台的 MessageQueue 发送任务。

2。使用BroadcastReceiver

此 API 旨在让 Android 应用可以发送和接收来自 Android 系统、应用内其他应用或组件(Activity、Service 等)的广播消息,类似于 publish-subscribe 设计伙伴。

因为BroadcastReceiver.onReceive(Context, Intent)方法默认是在主线程中调用的。因此,您可以使用它来更新主线程上的 UI。例如。

从后台线程发送数据。

// Send result from a background thread to the main thread
Intent intent = new Intent("ACTION_UPDATE_TEXT_VIEW");
intent.putExtra("text", "This is a test from a background thread");
getApplicationContext().sendBroadcast(intent);

从活动/片段接收数据

// Create a broadcast to receive message from the background thread
private BroadcastReceiver updateTextViewReceiver = new BroadcastReceiver() 
    @Override
    public void onReceive(Context context, Intent intent) 
        String text = intent.getStringExtra("text");
        myTextView.setText(text);
    
;

@Override
protected void onStart() 
    super.onStart();
    // Start receiving the message
    registerReceiver(updateTextViewReceiver, new IntentFilter("ACTION_UPDATE_TEXT_VIEW"));


@Override
protected void onStop() 
    // Stop receving the message
    unregisterReceiver(updateTextViewReceiver);
    super.onStop();

此方法通常用于Android应用程序之间或Android应用程序与系统之间的通信。实际上,你可以使用它来实现Android应用中组件之间的通信,例如(Activity、Fragment、Service、Thread等),但它需要大量的代码。

如果你想要一个类似的解决方案,但代码更少,易于使用,那么你可以使用下面的方法。

3。使用EventBus

EventBus 是适用于 Android 和 Java 的发布/订阅事件总线。如果要执行在主线程上运行的方法,只需用@Subscribe(threadMode = ThreadMode.MAIN)注解标记即可。

// Step 1. Define events
public class UpdateViewEvent 
    private String text;
    
    public UpdateViewEvent(String text) 
        this.text = text;
    

    public String getText() 
        return text;
    


// Step 2. Prepare subscriber, usually inside activity/fragment
@Subscribe(threadMode = ThreadMode.MAIN)  
public void onMessageEvent(MessageEvent event) 
    myTextView.setText = event.getText();
;

// Step 3. Register subscriber
@Override
public void onStart() 
    super.onStart();
    EventBus.getDefault().register(this);


// Step 4. Unregister subscriber
@Override
public void onStop() 
    super.onStop();
    EventBus.getDefault().unregister(this);


// Step 5. Post events from a background thread
UpdateViewEvent event = new UpdateViewEvent("new name");
EventBus.getDefault().post(event);

当您想要在用户可见活动/片段时更新视图(他们正在与您的应用交互)时,这很有用。

【讨论】:

非常完整的答案,不知道 EventBus! @BogdanAndroid 我添加了一个使用BroadcastReceiver的解决方案,你可以看看。【参考方案2】:

从一开始(API 1)开始,android 线程之间的通信方式一直是Handler。实际上AsyncTask 只是一个线程池的包装器,它使用Handler 也与主线程通信,您可以查看the source code 并类似地创建自己的包装器。

Handler 是非常低级的原语,我不会说使用Handler 是丑陋的,但它肯定需要一些多线程编程知识并使代码更加冗长。正如您还提到的那样,会出现很多问题,例如您的 UI 可能会在任务完成时消失,您必须自己处理。低级原语总是如此。

当您正在寻找有信誉的来源时,这里是 official documentation 就这个问题 - 将结果从后台线程传达给普通 java 中的主线程。

所以很遗憾,没有其他更好的官方推荐方法可以做到这一点。当然,还有很多像 rxJava 这样的 Java 库,它们构建在相同的原语之上,但提供了更高级别的抽象。

【讨论】:

【参考方案3】:

我个人是这样使用 AsyncTask 的:

    在我的 Activity 或 Fragment 中设置广播接收器 使用您选择的 Executor 在 Object[] 中使用任何需要的参数调用 asyncTask。 在 AsyncTask 完成 I Bundle 与数据或结果后,发送包含此包的 LocalBroadcast。 在我的片段或活动中接收广播并处理结果。 这种方法我从来没有遇到过任何问题,我知道有些人会回避 AsyncTask,但对于大多数用途而言,我所遇到的只是一种简单可靠的方法。

【讨论】:

我觉得这种方式很干净,没想过用broadcastReceiver。不错的答案!

以上是关于将后台线程的结果传达给 Android 中的 Ui 线程的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

Android 多线程 AsyncTask 完成后台任务并反馈给UI

android中的asynctask可不可以并行执行多个

android AsyncTask 怎么返回值给UI线程

Android 中AsyncTask后台线程的理解

Android中UI线程与后台线程交互设计的6种方法

通过AsyncTask访问后台后得到的返回数据在Android端显示