乐趣区

Android-Handler使用精析

1 引出问题

在启动一个 Android 应用的同时,便会开启一个主线程——Main Thread(也叫 UI 线程),主线程负责处理与 UI 相关的事件。

但是在开发中经常会做一些耗时的任务,这些耗时的任务会阻塞主线程,若长时间地阻塞主线程则会导致应用发生 ANR(应用程序无响应)。

因此我们需要将这些耗时任务放在子线程中去处理,并且在处理耗时任务的过程中,我们需要更新 UI 以便告知用户耗时任务的进度、状态等信息。

那么如何在子线程中更新主线程中的 UI 控件呢?
对此,我们可以借助 Handler 来完成。Handler 提供了三种方式来解决上述问题:

  1. 调用 Handler 的 sendMessage 方法;
  2. 调用 Handler 的 post 方法;
  3. 调用 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 方法进行处理。

参考资料:

  1. Android Handler 的基本使用
  2. Handler | Android Developers
  3. Android Handler 详解
退出移动版