从后台线程异常在TextView上设置文本

时间:2017-07-03 17:03:04

标签: java android multithreading thread-safety

我刚刚开始玩Android Concurrency / Loopers / Handers,我刚遇到了奇怪的异常现象。下面的代码不会阻止我在不同的Thread上设置TextView上的文本。

TextView tv;
Handler backgroundHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    tv = (TextView) findViewById(R.id.sample_text);
    Runnable run = new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            backgroundHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    String text = (String) msg.obj;
                    tv.setText(Thread.currentThread().getName() + " " + text);
                }
            };
            Looper.loop();
        }
    };

    Thread thread = new Thread(run);
    thread.setName("Background thread");
    thread.start();
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Message message = backgroundHandler.obtainMessage();
    message.obj = "message from UI";
    backgroundHandler.sendMessage(message);
}

猜猜发生了什么

enter image description here

但是,当我睡了一段时间背景线程时

backgroundHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String text = (String) msg.obj;
                tv.setText(Thread.currentThread().getName() + " " + text);
            }
        };

它会像我预期的那样抛出异常

07-03 18:54:40.506 5996-6025/com.stasbar.tests E/AndroidRuntime: FATAL EXCEPTION: Background thread
                                                             Process: com.stasbar.tests, PID: 5996
                                                             android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                                                                 at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7275)

有人可以解释一下发生了什么吗?为什么我能够从不同的线程设置文本?

2 个答案:

答案 0 :(得分:5)

从第一次尝试开始,您的代码就错了,因为您在不同的线程上创建了Handler。这导致looper / handler在不同的线程上运行。根据你的评论,我想你知道这个问题,你想要理解为什么异常不是第一次尝试而是第二次尝试。

您应该注意这一点:在UI Thread之外的其他线程上访问UI元素会导致未定义的行为。这意味着:

  • 有时你看到它有效。
  • 有时你没有。如你所见,你将遇到异常。

这意味着在不同的线程上访问UI元素并不总是100%可重现。

为什么你应该只在UI线程上访问所有UI元素?因为处理UI元素(更改内部状态,绘制到屏幕......)是一个复杂的过程,需要在相关方之间进行同步。例如,您在屏幕上可见的2个片段上调用TextView#setText(String)。 Android不会同时执行此操作,而是将所有作业推送到UI消息队列并按顺序执行。这不仅从您的应用程序角度而且从整个Android系统角度来看也是如此。从系统调用的状态栏进行更新,从应用程序调用的应用程序进行更新,始终将操作推送到相同 UI消息队列,然后再进行处理。

当您在不同的线程上访问和修改UI元素时,您就破坏了该过程。这意味着可能两个线程可能在同一状态和同一时间访问和修改元素。结果,您将在某个时间遇到竞争条件。发生错误时。

解释您的情况很难,因为没有足够的数据进行分析。但有几个原因:

  • 第一次尝试时,TextView尚未显示在屏幕上。所以不同的线程能够在TextView上进行更改。但是在第二次尝试时,你会睡1秒钟。此时,所有视图都已在屏幕上成功渲染并显示,因此异常抛出。您可以尝试Thread.sleep(0),希望您的代码不会崩溃。
  • 这种情况会在某些情况下发生,但很难猜到为什么。有一次机会,你的线程和ui线程都访问相同的锁对象,抛出异常。

您可以在此处阅读有关线程问题的更多信息Android Thread

  

明确参考

     

非主线程上的许多任务都有最终目标   更新UI对象。但是,如果其中一个线程访问了   在视图层次结构中的对象,应用程序不稳定可能导致:如果是   工作线程同时更改该对象的属性   任何其他线程引用该对象,结果是   未定义。

希望这对你有所帮助。

答案 1 :(得分:0)

只有UI线程才能对UI项进行编辑。换句话说,您无法从后台线程进行用户界面编辑。

因此,代替tv.setText(Thread.currentThread().getName() + " " + text);,在backgroundHandler中使用以下代码: -

 runOnUiThread(new Runnable() {
    public void run() {
        tv.setText(Thread.currentThread().getName() + " " + text);
    }
});