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详解

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理