Android基础:在UI线程中运行代码

500

从在UI线程中运行代码的角度来看,以下两种方式之间有什么区别:

MainActivity.this.runOnUiThread(new Runnable() {
    public void run() {
        Log.d("UI thread", "I am the UI thread");
    }
});
或者
MainActivity.this.myView.post(new Runnable() {
    public void run() {
        Log.d("UI thread", "I am the UI thread");
    }
});

private class BackgroundTask extends AsyncTask<String, Void, Bitmap> {
    protected void onPostExecute(Bitmap result) {
        Log.d("UI thread", "I am the UI thread");
    }
}

澄清我的问题:我假设这些代码是从一个服务线程调用的,通常是一个监听器。我还假设在AsynkTask的doInBackground()函数中或在第一个两个片段之前调用了一个新的Task(...),需要完成大量的工作。无论如何,AsyncTask的onPostExecute()被放置在事件队列的末尾,对吗? - Luky
8个回答

315

这三个方法并不完全相同,但它们都有相同的净效果。

第一个和第二个之间的差异在于,当执行代码时,如果你恰好处于主应用程序线程上,第一个方法(runOnUiThread())将立即执行Runnable。而第二个方法(post())总是将Runnable放在事件队列的末尾,即使你已经在主应用程序线程上。

第三个方法假设你创建并执行了一个BackgroundTask实例,它会浪费很多时间从线程池中获取一个线程,执行默认的无操作的doInBackground(),最终执行与post()相当的操作。这是三个方法中最不高效的。如果你确实需要在后台线程中做一些工作,而不仅仅是使用onPostExecute(),则请使用AsyncTask


28
请注意,AsyncTask.execute()方法需要在UI线程中调用,因此,除非您将所有后台工作移至doInBackground()方法并正确使用AsyncTask,否则该选项对于仅在后台线程上运行代码以在UI线程上运行的用例没有用处。 - kabuko
@kabuko 我该如何检查我是否从UI线程调用了AsyncTask? - Neil Galiaskarov
@NeilGaliaskarov 这看起来是一个可靠的选择:https://dev59.com/bnE85IYBdhLWcg3wSxgv#7897562 - Dick Lucas
1
@NeilGaliaskarov boolean isUiThread = (Looper.getMainLooper().getThread() == Thread.currentThread()); - ban-geoengineering
1
对于版本大于或等于M,请使用Looper.getMainLooper().isCurrentThread(),@NeilGaliaskarov - Balu Sangem

285

我喜欢来自HPP评论的代码,它可以在任何地方使用而不需要任何参数:

new Handler(Looper.getMainLooper()).post(new Runnable() {
    @Override
    public void run() {
        Log.d("UI thread", "I am the UI thread");
    }
});

5
这个有多有效?它和其他选项一样吗? - EmmanuelMess

65

使用Handler是第四种方法。

new Handler().post(new Runnable() {
    @Override
    public void run() {
        // Code here will run in UI thread
    }
});

65
你应该小心处理这个问题。因为如果你在非UI线程中创建一个handler,你将向非UI线程发送消息。 默认情况下,handler会将消息发布到它被创建的线程。 - lujop
115
要在主UI线程中执行,请使用new Handler(Looper.getMainLooper()).post(r),这是首选的方式,因为Looper.getMainLooper()会对主线程进行静态调用,而postOnUiThread()则必须在作用域内具有MainActivity的实例。 - HPP
1
@HPP 我不知道这个方法,当你既没有Activity也没有View时,这将是一个很好的方式。非常好用!非常感谢你! - Sulfkain
@lujop 同样适用于AsyncTask的onPreExecute回调方法。 - Sreekanth Karumanaghat

18

Pomber的答案是可以接受的,但我不太喜欢反复创建新对象。最好的解决方案总是试图减少内存占用。是的,有自动垃圾回收,但在移动设备上保护内存落实到最佳实践中。

下面的代码在服务中更新TextView。

TextViewUpdater textViewUpdater = new TextViewUpdater();
Handler textViewUpdaterHandler = new Handler(Looper.getMainLooper());
private class TextViewUpdater implements Runnable{
    private String txt;
    @Override
    public void run() {
        searchResultTextView.setText(txt);
    }
    public void setText(String txt){
        this.txt = txt;
    }

}

它可以在任何地方像这样使用:

textViewUpdater.setText("Hello");
        textViewUpdaterHandler.post(textViewUpdater);

9
提到了GC和对象创建,点个赞。不过,我并不完全同意“最好的解决方案总是试图减少内存占用”。还有许多其他标准可以定义“最好”,这似乎有点“过早优化”的味道。也就是说,除非你知道自己在调用它的次数足够多,以至于创建的对象数量成为问题(相比可能会造成垃圾的其他方式),否则“最好”的做法可能是编写最简单(易于理解)的代码,并转向其他任务。 - ToolmakerSteve
2
顺便说一下,textViewUpdaterHandler 最好改名为 uiHandlermainHandler,因为它通常适用于任何发布到主 UI 线程的内容;它与你的 TextViewUpdater 类没有任何关联。我会将其移出代码的其余部分,并明确说明它可以在其他地方使用... 代码的其余部分是可疑的,因为为了避免动态创建一个对象,你将本来可以是单个调用的步骤 setTextpost 分成了两步,这依赖于一个长期存在的对象,你将其用作临时对象。这样做增加了不必要的复杂性,而且不是线程安全的。难以维护。 - ToolmakerSteve
如果确实有这样的情况,需要多次调用 uiHandlertextViewUpdater 以值得进行缓存,请通过更改为 public void setText(String txt, Handler uiHandler) 并添加方法行 uiHandler.post(this); 来改进您的类。然后调用者可以通过一步完成:textViewUpdater.setText("Hello", uiHandler);。然后在将来,如果需要线程安全,方法可以在 uiHandler 上包装其语句内的锁定,并且调用者保持不变。 - ToolmakerSteve
我相信你只能运行一次Runnable。这绝对会破坏你的想法,总体而言还是不错的。 - Nativ
@Nativ 不对,Runnable 可以被多次运行。而 Thread 则不行。 - Zbyszek

13

从Android P开始,您可以使用getMainExecutor()函数:

getMainExecutor().execute(new Runnable() {
  @Override public void run() {
    // Code will run on the main thread
  }
});
Android开发者文档中获悉:
返回一个Executor,将在与此上下文关联的主线程上运行排队的任务。这是用于分派对应用程序组件(活动,服务等)的调用的线程。
CommonsBlog中获悉:
您可以在Context上调用getMainExecutor()来获取一个Executor,该Executor将在主应用程序线程上执行其作业。还有其他方法可以实现这一点,但使用Looper和自定义Executor实现更简单。

12

如果您需要在 Fragment 中使用,应该使用

private Context context;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        this.context = context;
    }


    ((MainActivity)context).runOnUiThread(new Runnable() {
        public void run() {
            Log.d("UI thread", "I am the UI thread");
        }
    });

代替

getActivity().runOnUiThread(new Runnable() {
    public void run() {
        Log.d("UI thread", "I am the UI thread");
    }
});

因为在某些情况下(例如分页器片段),会出现空指针异常。


8
使用Handler
new Handler(Looper.getMainLooper()).post(new Runnable() {
    @Override
    public void run() {
        // Code here will run in UI thread
    }
});

你选择使用通用的 Handler 而不是 runOnUiThread 有什么原因吗? - ThePartyTurtle
1
如果不是从UI线程调用,这将会产生一个java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() - Roc Boronat

1

Kotlin版本:

Handler(Looper.getMainLooper()).post {
   Toast.makeText(context, "Running on UI(Main) thread.", Toast.LENGTH_LONG).show()
}

如果您正在使用Kotlin协程: 在协程范围内添加以下内容:
withContext(Dispatchers.Main) {
   Toast.makeText(context, "Running on UI(Main) thread.", Toast.LENGTH_LONG).show()
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接