Android: Runnable多线程回调Callback更新UI时不工作的问题
概述
看名字有“多线程”和“更新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
调用Another
的newMessage()
,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
时,会调用Another
的newThread()
,此时newMessage()
在新的线程中执行,不再阻塞主线程。而回调还是老样子。
咋一看是不是没问题?实际运行就会提示在textView.setText(string);
这行出错:CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
。
原因分析
仔细想想原因很简单,当通过another.setCallback(self);
设置回调时,实际上是将调用此方法的MainActivity
的实例(记为mainActivity
)的引用传递给了another
(Another
的实例),而回调实际上是在another
中通过mainActivity
调用onNewMessage()
。注意,mainActivity
是在UI线程(主线程)创建的,但回调时是在新的线程对mainActivity
进行操作,那么更新UI也就在新线程中进行(因为没有出现线程切换),就出错了。
怎么样?问题是不是隐藏得有点深?(也可能是我笨没一眼看出来)上面的误区就在于以为mainActivity
一直在UI线程,只要在mainActivity
更新UI就不会有问题。实际情况是因为新线程和回调的存在,mainActivity
不一定一直在UI线程中运行。
解决方法
最简单的解决方法就是another
不要在新线程,直接在UI线程中运行就好了;但这样使得UI线程被阻塞,且回调也失去了本身的意义,不考虑。
借助于Android框架中的Handler
和Message
,我们可以轻松解决这个问题。
实现
我们不需要修改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
。
原理
关于Handler
和Message
这里不再具体介绍,已经有很多文章专门解释过其工作原理。我们来看看这是怎么运行的,在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的Handler
和Message
恰恰就是为应对这种情况,使用Handler
不仅不会让代码逻辑混乱,反而会使代码结构更清晰。
handler不能这么写,会内存泄漏的
嗯嗯的确可能会内存泄漏,应该使用WeakReference来声明handler,但这里为了说明问题的简便性考虑没有这么写~
谢谢分享!最近刚学习AysncTask, 刚这个好像!