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

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

想象一下,我想用我刚刚下载的一些信息更新一些。当我需要执行后台任务时,我使用了3种东西:TextView

异步任务

非常易于使用,这个方法可以将结果直接返回到UiThread,因此我可以使用回调接口或执行任何我想做的事情。我喜欢这个类,但它已被弃用。onPostExecute()

ThreadPoolExecutor

这就是我在需要执行后台任务时实际使用的内容,这就是我的问题,即我必须将结果提供给UiThread的那一刻。我已经告诉自己关于和类和关于.LooperHandlermainLooper

因此,当我需要返回一些结果时,我使用的方法,正如我所读到的,只需获取Ui线程并将其发布到队列中即可。runOnUiThread()LooperRunnable

好吧,这是有效的,我可以与主线程进行通信,但是,我发现它真的很丑陋,我相信有一种比填充我所有的“”方法代码更优雅的方法。另外,如果后台任务需要太多时间,也许用户已经更改了或当内部代码运行时会导致什么(我知道使用和模式可以解决最后一个问题,但我在遗留项目中工作,我无法重构所有代码,所以我正在使用分支活动mvc模式)runOnUiThread()ActivityFragmentrunOnUiThread()ExceptionsLiveDataMVVM

那么,还有另一种方法可以做到这一点吗?你能举个例子吗?我真的搜索了很多,但没有找到任何东西...

协程

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

任何帮助将不胜感激!


答案 1

背景

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

主线程有一个非常简单的设计:它唯一的工作就是从线程安全的工作队列中获取并执行工作块,直到其应用程序终止。该框架从各个位置生成其中一些工作块。这些位置包括与生命周期信息、用户事件(如输入)或来自其他应用和进程的事件关联的回调。此外,应用可以自行显式将块排队,而无需使用框架。

应用执行的几乎任何代码块都与事件回调(如输入、布局膨胀或绘制)相关联。当某些内容触发事件时,发生该事件的线程会将事件从自身中推出,并推入主线程的消息队列中。然后,主线程可以为事件提供服务。

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

要更新View,必须在主线程上执行此操作,如果您尝试在后台线程中更新,系统将抛出.CalledFromWrongThreadException

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

主线程有一个循环器和一个与之分配的消息队列。要更新视图,我们需要创建一个任务,然后将其放入 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 提供了几种方法:

在引擎盖下,他们使用Handler API来完成他们的工作。

回到你的问题

异步任务

这是一个设计为围绕线程和处理程序的帮助器类的类。它负责:

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

  • 创建与主线程关联的处理程序,以将任务发送到主线程的消息队列。

  • 它已从 API 级别 30 弃用

ThreadPoolExecutor

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

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

Kotlin Coroutines

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

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

1. 使用基于处理程序构建的处理程序或机制

1.1. 如果线程以活动/片段为界:

1.2. 如果线程具有对视图的引用,例如适配器类。

1.3. 如果线程未绑定到任何 UI 元素,则自行创建一个处理程序。

Handler mainHandler = new Handler(Looper.getMainLooper);

注意:使用处理程序的一个好处是,您可以使用它在线程之间执行双向通信。这意味着从后台线程可以将任务发送到主线程的 MessageQueue,从主线程,您可以将任务发送到后台的 MessageQueue。

2. 使用广播接收器

此 API 旨在允许 Android 应用从应用内的其他应用或组件(活动、服务等)发送和接收来自 Android 系统、其他应用或组件(活动、服务等)的广播消息,类似于发布-订阅设计部分。

由于 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应用程序中的组件之间进行通信,例如(活动,片段,服务,线程等),但它需要大量的代码。

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

3. 使用事件总线

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);

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


答案 2

从一开始(API 1),线程之间的Android通信方式就是Handler。实际上只是一个围绕线程池的包装器,它还用于与主线程通信,您可以查看源代码并以类似的方式创建自己的包装器。AsyncTaskHandler

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

当您正在寻找信誉良好的来源时,这里是关于这个问题的官方文档 - 在纯java中将结果从后台线程传达给主线程。

因此,不幸的是,没有其他 - 更好和官方推荐的 - 方法来做到这一点。当然,有很多像rxJava这样的Java库建立在相同的原语之上,但提供了更高层次的抽象。


推荐