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

Correct way to communicate the result of a background thread to the Ui Thread in Android

这是我最困惑的话题之一。 所以我的问题是,当这个完成时,传递后台线程结果的正确方法是什么

假设我想用一些信息更新一些 TextView 我只是 downloaded.There 是我在需要执行后台任务时使用的 3 件事:

异步任务

非常容易使用,这个有 onPostExecute() 方法,将 return 结果直接发送到 Ui 线程,所以我可以使用回调接口或做任何我想做的事.我喜欢这个 class 但它 已弃用。

线程池执行器

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

所以,当我需要 return 一些结果时,我使用 runOnUiThread() 方法,正如我所读,只需获取 Ui 线程的 Looper和 post 我的 Runnable 到队列中。

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

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

协程

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

如有任何帮助,我们将不胜感激!

从一开始 (API 1) 线程之间的 android 通信方式就是 Handler. Actually AsyncTask is just a wrapper around a thread pool and it uses Handler also to communicate to the main thread, you can check out the source code 并类似地创建您自己的包装器。

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

当您正在寻找有信誉的来源时,这里是 official documentation 正是关于这个问题 - 以普通 java.

将后台线程的结果传递给主线程

所以不幸的是,没有其他更好的官方推荐的方法来做到这一点。当然,有很多 java 库,例如 rxJava,它们构建在相同的原语之上,但提供了更高级别的抽象。

背景

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

The main thread has a very simple design: Its only job is to take and execute blocks of work from a thread-safe work queue until its app is terminated. The framework generates some of these blocks of work from a variety of places. These places include callbacks associated with lifecycle information, user events such as input, or events coming from other apps and processes. In addition, app can explicitly enqueue blocks on their own, without using the framework.

Nearly any block of code your app executes is tied to an event callback, such as input, layout inflation, or draw. When something triggers an event, the thread where the event happened pushes the event out of itself, and into the main thread’s message queue. The main thread can then service the event.

While an animation or screen update is occurring, the system tries to execute a block of work (which is responsible for drawing the screen) every 16ms or so, in order to render smoothly at 60 frames per second. For the system to reach this goal, the UI/View hierarchy must update on the main thread. However, when the main thread’s messaging queue contains tasks that are either too numerous or too long for the main thread to complete the update fast enough, the app should move this work to a worker thread. If the main thread cannot finish executing blocks of work within 16ms, the user may observe hitching, lagging, or a lack of UI responsiveness to input. If the main thread blocks for approximately five seconds, the system displays the Application Not Responding (ANR) dialog, allowing the user to close the app directly.

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

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

主线程分配了一个 Looper and a 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 提供了几种方法:

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

回到你的问题

AsyncTask

这是一个class,旨在作为class围绕Thread和Handler的助手。它负责:

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

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

  • 已从 API 级别 30

    弃用

ThreadPoolExecutor

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

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

Kotlin Coroutines

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

So my question is, what is the correct way of communicate the result of background thread when this finish?.

1.使用 Handler 或基于 Handler

构建的机制

1.1. 如果一个线程被绑定 Activity/Fragment:

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

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

Handler mainHandler = new Handler(Looper.getMainLooper);

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

2。使用 BroadcastReceiver

此 API 旨在允许 Android 应用程序可以发送和接收来自 Android 系统、其他应用程序或组件(Activity、服务等)的广播消息) 在应用内,类似于 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);

从activity/fragment

接收数据
// 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 app中的组件之间的通信,例如(Activity、Fragment、Service、Thread等),但是需要大量的代码。

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

3。使用 EventBus

EventBus 是 publish/subscribe 用于 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);

如果您想在 activity/fragment 对用户可见(他们正在与您的应用程序交互)时更新视图,这会很有用。

我个人是这样使用AsyncTask的:

    • 在我的 Activity 或 Fragment
    • 中设置一个 broadcastReceiver
    • 使用您选择的执行器在 Object[] 中使用任何需要的参数调用 asyncTask。
    • 一旦 AsyncTask 完成 I Bundle with the data or result,发送包含此 bundle 的 LocalBroadcast。
    • 在我的 Fragment 或 activity 中接收广播并处理结果。 我从来没有遇到过这种方法的任何问题,我知道有些人正在回避 AsyncTask,但对于大多数目的和我所遇到的一切来说,这是一种简单可靠的方法。