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

Bog*_*oid 11 java multithreading android android-asynctask threadpoolexecutor

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

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

异步任务

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

线程池执行器

这就是我在需要执行后台任务时实际使用的方法,而我的问题就出现在我必须将结果提供给 UiThread 的那一刻。我自己了解LooperHandler类以及有关mainLooper

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

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

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

协程

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

任何帮助,将不胜感激!

Nhấ*_*ang 14

背景

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

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

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

在发生动画或屏幕更新时,系统会尝试每 16 毫秒左右执行一个工作块(负责绘制屏幕),以便以每秒 60 帧的速度平滑渲染。为了让系统达到这个目标,UI/View 层次结构必须在主线程上更新。但是,当主线程的消息队列包含太多或太长的任务时,主线程无法足够快地完成更新,应用程序应该将此工作移至工作线程。如果主线程无法在 16 毫秒内完成工作块的执行,用户可能会观察到卡顿、滞后或缺乏 UI 对输入的响应。如果主线程阻塞大约 5 秒钟,系统会显示应用程序无响应 (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
    }
});
Run Code Online (Sandbox Code Playgroud)

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

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

回到你的问题

异步任务

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

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

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

  • 它已从 API 级别 30 开始弃用

线程池执行器

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

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

Kotlin 协程

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

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

1.使用Handler或者建立在Handler之上的机制

1.1. 如果线程受 Activity/Fragment 限制:

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

1.3. 如果线程未绑定到任何 UI 元素,则自己创建一个 Handler。

Handler mainHandler = new Handler(Looper.getMainLooper);
Run Code Online (Sandbox Code Playgroud)

注意:使用 Handler 的一个好处是您可以使用它在线程之间进行两种方式的通信。这意味着您可以从后台线程向主线程的 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);
Run Code Online (Sandbox Code Playgroud)

从活动/片段接收数据

// 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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

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);
Run Code Online (Sandbox Code Playgroud)

当您想在 Activity/Fragment 对用户可见(他们正在与您的应用程序交互)时更新视图时,这很有用。

  • 非常完整的答案,不知道 EventBus! (2认同)

ese*_*sov 7

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

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

当您正在寻找信誉良好的来源时,这里是官方文档- 用纯 java 将结果从后台线程传递到主线程。

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


Dam*_*oke 5

我个人是这样使用AsyncTask的:

    • 在我的 Activity 或 Fragment 中设置广播接收器
    • 使用您选择的执行器在 Object[] 中调用带有任何所需参数的 asyncTask。
    • 一旦 AsyncTask 完成了数据或结果的捆绑,发送包含该捆绑的 LocalBroadcast。
    • 在我的片段或活动中接收广播并处理结果。我对这种方法从来没有遇到过任何问题,我确实理解有些人回避 AsyncTask,但对于大多数目的和我遇到的所有情况来说,这是一种简单而可靠的方法。