Android系统是单线程模型,更新UI的操作只能在主线程内操作,并且如果在主线程内进行耗时操作容易造成应用程序无响应(ANR)。
一般的解决办法是:在主线程中开启子线程,子线程来进行耗时操作。
由于在Android中只能由主线程来更新UI,因此耗时操作产生结果后,子线程应该通知主线程进行相应的UI更新。
主(父)线程和子线程,以及子线程之间需要进行数据交换等通信,这个任务在Android中是交给Handler来完成的。
一、子线程通知主线程更新UI
方法1:sendMessage()
首先,在界面上放置一个TextView和Button。TextView用来显示通信结果,Button用来启动通信以及区分线程通信类别。
activity_main.xml
<?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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/main_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="28sp"
android:textAlignment="center"/>
<Button
android:id="@+id/btn_send"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="send"
android:textSize="28sp"/>
</LinearLayout>
然后呢,在MainActivity代码中先定义一个整型常量 MSG_SEND = 0 作为区分通信Message类别的标识。
然后定义一个Handler类型的私有成员变量mHandler作为主线程和子线程通信的工具,mHandler在主线程中定义,因此是属于主线程的;稍后会看到子线程也是使用此变量来发送消息。
重写Handler类的handleMessage(@NonNull Message msg)方法:msg是接收到的Message消息,msg.what是消息发送方设定的消息类型,方法中使用switch区分msg.what的值,针对不同的消息类型进行不同处理。方法接收到MSG_SEND类型的消息时,将其传递的obj对象(实际上是字符串对象)字符串化并在mainTitle(TextView)上显示。
public class MainActivity extends AppCompatActivity {
private final int MSG_SEND = 0; // sendMessage
private TextView mainTitle;
private Button sendBtn;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
switch(msg.what){
case MSG_SEND:
// 对UI进行更新操作
mainTitle.setText(msg.obj.toString());
break;
}
}
};
...
}
接下来在onCreate()方法中,进行视图控件的初始化操作,并在按钮事件中启动一个自定义的SendThread线程。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainTitle = findViewById(R.id.main_title);
sendBtn = findViewById(R.id.btn_send);
sendBtn.setOnClickListener(v -> {
new SendThread().start(); // 启动子线程
});
}
最后呢,当然是自定义线程啦。
继承Thread并重写run()方法,首先使线程睡眠1秒钟,然后新建Message消息,类型what设置为MSG_SEND,携带的对象obj设置为字符串对象"sendMessage"(这里应该是用到了Java特性——装箱),最后通过调用mHandler.sendMessage(msg)来将msg发送给主线程。
public class MainActivity extends AppCompatActivity {
...
class SendThread extends Thread {
@Override
public void run() {
// 线程睡眠1秒
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 发送信息
Message msg = new Message();
msg.what = MSG_SEND;
msg.obj = "sendMessage";
mHandler.sendMessage(msg);
}
}
}
最终效果:
方法2:post()
首先呢,在布局上再添加一个按钮,来启动post方法的子线程。
<Button
android:id="@+id/btn_post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="post"
android:textSize="28sp"/>
然后设置按钮事件,启动自定义PostThread线程。
postBtn = findViewById(R.id.btn_post);
postBtn.setOnClickListener(v -> {
new PostThread().start();
});
最后定义PostThread。
在run()方法中,调用mHandler的post方法,并传入Runnable匿名内部类(这里使用Lambda表达式进行了简化),在匿名内部类的run() 方法(实际上是在主线程中运行的)中可以直接调用主线程中定义的控件进行UI上的操作。
class PostThread extends Thread {
@Override
public void run() {
// 线程睡眠1秒
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mHandler.post(() -> {
// 直接调用主线程的控件进行对应UI更新操作
mainTitle.setText("Post success.");
});
}
}
最终效果:
方法3:obtainMessage()
obtainMessage()和sendMessage()类似,只不过在子线程中发送消息的方式不同,这里也按流程走一遍。
首先呢,在布局上再添加一个按钮,来启动obtainMessage方法的子线程。
<Button
android:id="@+id/btn_obtain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="obtain"
android:textSize="28sp"/>
然后呢,在MainActivity代码中定义一个整型常量 MSG_OBTAIN = 1 作为区分obtainMessage方法的标识,并在handleMessage(@NonNull Message msg)对对应的消息进行处理。这里将消息携带的arg1整型参数显示在屏幕上。
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
switch(msg.what){
case MSG_SEND:
// 对UI进行更新操作
mainTitle.setText(msg.obj.toString());
break;
case MSG_OBTAIN:
mainTitle.setText(String.valueOf(msg.arg1));
break;
}
}
};
然后设置按钮事件,启动自定义ObtainThread线程。
obtainBtn = findViewById(R.id.btn_obtain);
obtainBtn.setOnClickListener(v -> {
new ObtainThread().start();
});
最后定义ObtainThread。
这里调用了mHandler.obtainMessage(MSG_OBTAIN, 110, 0).sendToTarget()方法。
首先obtainMessage方法是获取Message的方法,有:
obtainMessage(),
obtainMessage(int what),
obtainMessage(int what, @Nullable Object obj),
obtainMessage(int what, int arg1, int arg2),
obtainMessage(int what, int arg1, int arg2, @Nullable Object obj)
一共5种构造方法,按需选择。
class ObtainThread extends Thread{
@Override
public void run() {
// 线程睡眠1秒
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mHandler.obtainMessage(MSG_OBTAIN, 110, 0).sendToTarget();
}
}
对于sendToTarget()方法呢,查看源码,实际上就是调用了sendMessage方法,但是此方法相对于sendMessage而言更简洁。
public void sendToTarget() { target.sendMessage(this); }
最终效果:
二、子线程间的相互通信
原理
其实在Handler通信机制中,存在这样的对应关系:
Thread——Looper——MessageQueue 是1对1对1的;
MessageQueue——message 和 Thread——Handler 是1对N的。
在创建主线程ActivityThread时默认会先将Looper准备好(Looper.prepareMainLooper()),所以在主线程中可以直接使用handler;
而对于子线程则需要手动来准备和运行Looper(Looper.prepare() 和 Looper.loop())。
主线程本质上是ActivityThread,其源代码的main方法如下:
/frameworks/base/core/java/android/app/ActivityThread.java
public static void main(String[] args) {
...
Looper.prepareMainLooper();
...
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
...
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
从上述代码中可以看到,主线程也使用到了Looper,只不过是在主线程运行前已经准备好了Looper,无需手动准备和运行。
因此,这里讨论主线程向子线程发送消息的过程(即子线程持有Handler)。
使用
首先按照惯例添加发送按钮:
<Button
android:id="@+id/btn_sub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="sub"
android:textSize="28sp"/>
然后,定义成员变量subThread,并在onCreate()方法中启动。
在按钮的事件中构建Message并通过subThread的mHandler成员进行发送。
(此处上下文是主线程,并且不持有Handler)
subThread = new SubThread();
subThread.start();
subBtn = findViewById(R.id.btn_sub);
subBtn.setOnClickListener(v -> {
Message msg = new Message();
msg.what = MSG_SEND;
msg.obj = "subMessage";
subThread.mHandler.sendMessage(msg);
});
SubThread的定义如下,在run()方法开始时调用Looper.prepare(),使此线程初始化为一个Looper;
最后调用Looper.loop()方法运行消息队列;
对mHandler进行了初始化,并处理传递过来的消息(打印Log)。
class SubThread extends Thread {
public Handler mHandler;
@Override
public void run() {
super.run();
// 初始化此线程为Looper
Looper.prepare();
mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_SEND:
Log.d("SubThread", "msg = " + msg.obj);
break;
default:
break;
}
}
};
// 在此线程中运行消息队列
Looper.loop();
}
}
最终效果:
三、Handler的内存泄漏
实际上本文的代码仍有不足之处,即存在内存泄漏的风险,AS也会智能进行提示(如下图)。
后续我会继续出一篇关于解决Handler内存泄漏的文章。
制作不易,如果觉得本文对你有所帮助,那就点个赞吧!
欢迎在评论区提出更好的方法!