共计 6740 个字符,预计需要花费 17 分钟才能阅读完成。
1 引出问题
在启动一个 Android 应用的同时,便会开启一个主线程——Main Thread(也叫 UI 线程),主线程负责处理与 UI 相关的事件。
但是在开发中经常会做一些耗时的任务,这些耗时的任务会阻塞主线程,若长时间地阻塞主线程则会导致应用发生 ANR(应用程序无响应)。
因此我们需要将这些耗时任务放在子线程中去处理,并且在处理耗时任务的过程中,我们需要更新 UI 以便告知用户耗时任务的进度、状态等信息。
那么如何在子线程中更新主线程中的 UI 控件呢?
对此,我们可以借助 Handler 来完成。Handler 提供了三种方式来解决上述问题:
- 调用 Handler 的 sendMessage 方法;
- 调用 Handler 的 post 方法;
- 调用 Handler 的 obtainMessage 方法;
2 Handler 的简单使用
2.1 调用 sendMessage 方法
为了更好地理解 Handler 的使用,我们创建一个 Demo 来做一个简单的演示示例。点击屏幕上的按钮开始执行任务,同时文本框显示“开始执行任务”,用休眠 5 秒钟模拟执行耗时任务,当任务执行完(休眠结束后),文本框显示“任务执行完毕”。
布局文件代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tooltip_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/start_execute_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始执行" />
</LinearLayout>
MainActivity 的代码编写如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "MA-cxs";
// 工具提示文本框
private TextView tooltipTv;
// 开始执行任务按钮
private Button startExecute;
// 是否开始执行
private boolean isExecute = false;
public final int MSG_EXECUTE_START = 1000;
public final int MSG_EXECUTE_COMPLETE = 1001;
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();}
/**
* 初始化控件
*/
private void initViews() {tooltipTv = findViewById(R.id.tooltip_tv);
startExecute = findViewById(R.id.start_execute_btn);
startExecute.setOnClickListener(this);
}
@Override
public void onClick(View view) {if (view.getId() == R.id.start_execute_btn) {if (!isExecute) {new MyThread().start();}
}
}
/**
* 创建一个执行耗时任务的子线程,并发送消息
*/
class MyThread extends Thread {
@Override
public void run() {
isExecute = true;
Log.d(TAG, "子线程开始执行");
// 发送消息给 Handler
mExecuteTaskHandler.sendEmptyMessage(MSG_EXECUTE_START);
// 借助休眠模拟执行任务的过程
try {Thread.sleep(5000);
} catch (InterruptedException e) {e.printStackTrace();
}
// 执行完任务后再次发送一个执行成功的消息
Message message = new Message();
// 此处也可设置 message.arg1、message.arg2、message.obj、message.setData(Bundle 对象)方法
message.what = MSG_EXECUTE_COMPLETE;
message.setData(new Bundle());
mExecuteTaskHandler.sendMessage(message);
isExecute = false;
Log.d(TAG, "子线程执行完毕");
}
}
// 接收消息并进行处理
private Handler mExecuteTaskHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {switch (msg.what) {
case MSG_EXECUTE_START:
Log.d(TAG, "收到开始执行任务的消息");
tooltipTv.setText("开始执行任务");
break;
case MSG_EXECUTE_COMPLETE:
Log.d(TAG, "收到任务执行完毕的消息");
tooltipTv.setText("任务执行完毕");
break;
}
}
};
}
Handler 的使用步骤总结:
1.发送消息:在执行耗时任务时发送消息给给 Handler;
2.接收消息并进行处理:在主线程 (UI 线程) 中创建一个 Handler 对象,并实现其 handleMessage()方法,并且根据 message 参数 (what、arg1、arg2 或 obj) 的不同进行相应的处理——更新 UI。
设置 Message 除了可以设置其 what、arg1 及 arg2 之外,还可以借助 Message 的 setData 方法,传入一个 Bundle 对象。
发送消息的方法除了有 sendMessage 外还有其他的方法:
修饰符和返回值类型 | 方法及其描述 |
---|---|
public final boolean | sendEmptyMessage(int what) 发送仅包含 what 值的消息 |
public final boolean | sendEmptyMessageAtTime(int what, long uptimeMillis) 发送仅包含 what 值并且在 指定的绝对时间 传递的消息 |
public final boolean | sendEmptyMessageDelayed(int what, long delayMillis) 仅包含 what 值的消息,并且该消息将在延迟指定的时间后发送 |
public final boolean | sendMessage(Message msg) 将消息放在当前待处理的消息队列的末尾 |
public final boolean | sendMessageAtFrontOfQueue(Message msg) 将消息放入消息队列的最前面,以在消息循环的下一次迭代中进行处理 |
public boolean | sendMessageAtTime(Message msg, long uptimeMillis) 在指定的 绝对运行时间 发送消息 |
public final boolean | sendMessageDelayed(Message msg, long delayMillis) 在延迟指定的时间后发送消息 |
对于延时、定时消息,有时候需要取消,则可以通过以下方法将指定消息移除:
修饰符和返回值类型 | 方法及其描述 |
---|---|
public final void | removeCallbacksAndMessages(Object token) 移除 obj 为 token 的任何待处理的回调及已发送的消息 |
public final void | removeMessages(int what) 删除消息队列中为 what 参数为 what 值的待处理的消息 |
public final void | removeMessages(int what, Object object) 删除消息队列中为 what 参数 what 值并且 obj 参数为 object 的待处理的消息 |
2.2 调用 post 方法
则改写上述代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
// 其余代码不变
@Override
public void onClick(View view) {if (view.getId() == R.id.start_execute_btn) {if (!isExecute) {//new MyThread().start();
new PostThread().start();
}
}
}
class PostThread extends Thread {
@Override
public void run() {
isExecute = true;
Log.d(TAG, "PostThread run(): ThreadId=" + Thread.currentThread().getId() +
", ThreadName=" + Thread.currentThread().getName());
// 发送消息给 Handler
mExecuteTaskHandler.post(new Runnable() {
@Override
public void run() {Log.d(TAG, "Runnable run(): ThreadId=" + Thread.currentThread().getId() +
", ThreadName=" + Thread.currentThread().getName());
tooltipTv.setText("开始执行任务");
}
});
// 借助休眠模拟执行任务的过程
try {Thread.sleep(5000);
} catch (InterruptedException e) {e.printStackTrace();
}
// 执行完任务后再次发送一个执行成功的消息
Message message = new Message();
// 此处也可设置 message.arg1、message.arg2、message.obj、message.setData(Bundle 对象)方法
message.what = MSG_EXECUTE_COMPLETE;
message.setData(new Bundle());
mExecuteTaskHandler.post(new Runnable() {
@Override
public void run() {Log.d(TAG, "Runnable run(): ThreadId=" + Thread.currentThread().getId() +
", ThreadName=" + Thread.currentThread().getName());
tooltipTv.setText("任务执行完毕");
}
});
isExecute = false;
}
}
}
运行应用程序后点击按钮,打印日志如下:
PostThread run(): ThreadId=154, ThreadName=Thread-2
Runnable run(): ThreadId=2, ThreadName=main
Runnable run(): ThreadId=2, ThreadName=main
总结:
从上面代码并结合日志可以看出:Handler 的 post 方法参数为一个 Runnable 对象,由于 Handler 是在主线程中创建的,因此,Runnable 也是在主线程中运行,则 Runnable 与创建它的线程无关,与调用 post 方法的线程无关。并且 Runnable 的 run 方法是在主线程中更新 UI 的。
与 sendMessage 方法类似,post 方法也有多个相似的方法:
修饰符和返回值类型 | 方法及其描述 |
---|---|
public final boolean | post(Runnable r) 将 Runnable 对象添加到消息队列中 |
public final boolean | postAtFrontOfQueue(Runnable r) 将 Runnable 对象添加到消息队列的最前面 |
public final boolean | postAtTime(Runnable r, long uptimeMillis) 将 Runnable 对象添加到消息队列中,并且在 指定的绝对时间 执行 |
public final boolean | postAtTime(Runnable r, Object token, long uptimeMillis) 同上 |
public final boolean | postDelayed(Runnable r, long delayMillis) 将 Runnable 对象添加到消息队列中,并在经过指定的时间后运行 |
public final boolean | postDelayed(Runnable r, Object token, long delayMillis) 同上 |
同 sendMessage 方法,可以通过 removeCallbacks(Runnable r)、removeCallbacks(Runnable r, Object token)及 removeCallbacksAndMessages(Object token)方法取消 post 定时、延时处理的 Runnable。
2.3 调用 obtainMessage 方法
obtainMessage 方法与 sendMessage 方法类似,也可以看成是一种。通过下面的代码就能看出这一点:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
// 其余代码不变
@Override
public void onClick(View view) {if (view.getId() == R.id.start_execute_btn) {if (!isExecute) {// new MyThread().start();
// new PostThread().start();
new ObtainThread().start();
}
}
}
class ObtainThread extends Thread {
@Override
public void run() {
isExecute = true;
Log.d(TAG, "PostThread run(): ThreadId=" + Thread.currentThread().getId() +
", ThreadName=" + Thread.currentThread().getName());
// 发送消息给 Handler
mExecuteTaskHandler.obtainMessage(MSG_EXECUTE_START).sendToTarget();
// 借助休眠模拟执行任务的过程
try {Thread.sleep(5000);
} catch (InterruptedException e) {e.printStackTrace();
}
// 执行完任务后再次发送一个执行成功的消息
mExecuteTaskHandler.obtainMessage(MSG_EXECUTE_COMPLETE).sendToTarget();
isExecute = false;
}
}
}
总结:
通过调用 obtainMessage 方法即可生成 Message 对象,此对象携带其 target 对象,通过调用 sendToTarget 方法即可将消息发送到 Handler 的消息队列中,然后再由 Handler 的 handleMessage 方法进行处理。
参考资料:
- Android Handler 的基本使用
- Handler | Android Developers
- Android Handler 详解