概述

看名字有“多线程”和“更新UI”想必大家都知道问题很简单:UI只能在UI线程中更新,而不能在其他线程中。但有时候这种问题不太明显,比如在Runnable中通过Callback来更新UI,很容易就误以为是在UI线程中更新。

问题描述

简单Callback

先来看一个简单的Callback的例子。

layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.polarxiong.passmessage.MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button" />
</LinearLayout>

layout中有一个TextView,用来展示信息;以及一个Button,点击此按钮后就会通过回调更新TextView的内容。

Another

public class Another {
    private Callback mCallback;

    public interface Callback {
        void onNewMessage(String string);
    }

    public void setCallback(Callback callback) {
        mCallback = callback;
    }

    public void newMessage() {
        if (mCallback != null) {
            mCallback.onNewMessage("message from Another");
        }
    }
}

Another类定义一个回调接口,通过setCallback()设置回调,通过newMessage()进行回调。

MainActivity

public class MainActivity extends AppCompatActivity implements Another.Callback {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final MainActivity self = this;
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Another another = new Another();
                another.setCallback(self);
                another.newMessage();
            }
        });
    }

    private void updateUI(String string) {
        TextView textView = (TextView) findViewById(R.id.text_view);
        textView.setText(string);
    }

    @Override
    public void onNewMessage(String string) {
        updateUI(string);
    }
}

MainActivity类继承Another.Callback接口,回调即onNewMessage(),用来调用updateUI()更新UI。对Button监听点击事件,当触发时,实例化Another类,设置回调,并调用newMessage()

这样在点击Button时,MainActivity调用AnothernewMessage()newMessage()随即回调onNewMessage(),让MainActivity更新UI。APP运行时效果就是点击按钮后Hello World!变为message from Another。这是一个简单的回调,很容易理解。

Runnable

但上面例子的实际情况是,调用Another和回调都在主线程,即UI线程完成,在调用时会阻塞主线程。而实际情况中,我们希望被调用的方法能够在新的线程中完成,不阻塞主线程,通过回调与调用者通信。看下面的例子。

Another

public class Another implements Runnable {
    private Callback mCallback;

    public interface Callback {
        void onNewMessage(String string);
    }

    public void setCallback(Callback callback) {
        mCallback = callback;
    }

    public void newMessage() {
        if (mCallback != null) {
            mCallback.onNewMessage("message from Another");
        }
    }

    public void newThread() {
        Thread childThread = new Thread(this, "Another");
        childThread.start();
    }

    public void run() {
        newMessage();
    }
}

Another类相比于之前,多了继承Runnable接口,run()会执行newMessage();而newThread()则会创建一个新的线程并启动这个线程,就是在新的线程中执行newMessage()了。

MainActivity
相比于之前,只需要将

another.newMessage();

修改为

another.newThread();

这样,当点击Button时,会调用AnothernewThread(),此时newMessage()在新的线程中执行,不再阻塞主线程。而回调还是老样子。

咋一看是不是没问题?实际运行就会提示在textView.setText(string);这行出错:CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

原因分析

仔细想想原因很简单,当通过another.setCallback(self);设置回调时,实际上是将调用此方法的MainActivity的实例(记为mainActivity)的引用传递给了anotherAnother的实例),而回调实际上是在another中通过mainActivity调用onNewMessage()。注意,mainActivity是在UI线程(主线程)创建的,但回调时是在新的线程对mainActivity进行操作,那么更新UI也就在新线程中进行(因为没有出现线程切换),就出错了

怎么样?问题是不是隐藏得有点深?(也可能是我笨没一眼看出来)上面的误区就在于以为mainActivity一直在UI线程,只要在mainActivity更新UI就不会有问题。实际情况是因为新线程和回调的存在,mainActivity不一定一直在UI线程中运行。

解决方法

最简单的解决方法就是another不要在新线程,直接在UI线程中运行就好了;但这样使得UI线程被阻塞,且回调也失去了本身的意义,不考虑。

借助于Android框架中的HandlerMessage,我们可以轻松解决这个问题。

实现

我们不需要修改Another类,将MainActivity类修改为

public class MainActivity extends AppCompatActivity implements Another.Callback {
    final Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            String str = (String) msg.obj;
            updateUI(str);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final MainActivity self = this;
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Another another = new Another();
                another.setCallback(self);
                another.newThread();
            }
        });
    }

    private void updateUI(String string) {
        TextView textView = (TextView) findViewById(R.id.text_view);
        textView.setText(string);
    }

    @Override
    public void onNewMessage(String string) {
        Message msg = handler.obtainMessage();
        msg.obj = string;
        handler.sendMessage(msg);
    }
}

相比于之前,增加了一个handler变量,此变量实例化了一个Handler(),并重载了handleMessage()处理接收到的Message的方法,即从Message中获取字符串信息,并调用updateUI()更新UI。对应在onNewMessage()中,每次回调都会构建一个新的Message,将回调的信息加入Message中,然后通过Handler发送此Message

这样,APP运行时点击按钮后Hello World!就变成了message from Another

原理

关于HandlerMessage这里不再具体介绍,已经有很多文章专门解释过其工作原理。我们来看看这是怎么运行的,在MainActivity实例化时会创建handler,而此时一定在UI线程,所以handler工作在UI线程,即handleMessage()的执行一定在UI线程,所以updateUI()不会有问题。

Handler的好处在哪呢?Handler在处理接收到的Message时一定在创建Handler的线程,而向Handler加入Message,则可以在任意线程。也就是说,无论在哪个线程中,只要能够获取到handler,那么在向handler发送Message后,这个Message就一定会在创建handler的线程被取出并进行处理。

就如上的例子来看,onNewMessage()回调是在新线程中,但此时我们能够获取到handler,所以讲回调的信息包装成Message交给这个handler;而handler收到这个Message后,就会在UI线程对其进行处理,即调用handleMessage(),这时在UI线程执行updateUI(),更新UI,完成。

小结

本篇主要分析了Runnable多线程回调Callback更新UI时不工作或出错的问题,原因在于多线程的切换导致更新UI不在UI线程中执行。而Android的HandlerMessage恰恰就是为应对这种情况,使用Handler不仅不会让代码逻辑混乱,反而会使代码结构更清晰。

参考