Android handler 消息通信实践(附计时器 demo)

Maenj_Ba_lah

**segmentfault 对 mackdown 语法的支持不是很好,有些图片都显示不出来,大家可以去我的掘金查看这篇文章。

一、Android 中的 UI 线程概述

<font face="黑体">Android 的 UI 线程是线程不安全的,也就是说想要更新应用程序中的 UI 元素,则必须在主线程中进行。所以主线程又叫做 UI 线程。若在子线程中更新 UI 程序会报错。但是我们经常有这样一种需求:需要在子线程中完成一些耗时任务后根据任务执行结果来更新相应的UI。这就需要子线程在执行完耗时任务后向主线程发送消息,主线程来更新UI。也就是线程之间的通信,线程间通信方法有很多,今天我们主要来讲利用 Handler 来实现线程之间的通信。

二、常用类

1、Handler

<font face="黑体">Handler 是 Android 消息机制的上层接口,通过 handler,可以将一个任务切换到 handler 所在的线程中执行,我们通常使用 handler 来更新 UI,但更新 UI 仅仅是的使用场景之一,handler 并不是仅仅用来更新 UI 的。

1)、在子线程发送消息去主线程更新 UI

<font face="黑体">我们都知道 Android 中的网络操作是要在子线程中进行的,当我们请求到网络数据以后,肯定需要展示数据到界面上。我们这里的网络请求就以 HttpURLConnection 那篇博文中的网络请求为例子。

<font face="黑体">实现效果如下所示:
Handler通信
<font face="黑体">具体步骤如下:

<font face="黑体">1、创建 Handler

// 1:实例化
// 2:在子线程中发送(空)消息
// 3:由 Handler 对象接收消息,并处理

// 只要 Handler 发消息了,必然会触发该方法,并且会传入一个 Message 对象
@SuppressLint("HandlerLeak")
// import android.os.Handler;  注意是 os 包下的 Handler
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
        }
    }
};

<font face="黑体">2、网络请求到数据后发送(空)消息

new Thread() {
    @Override
    public void run() {
        super.run();
        strMsg = get();
        Log.e("MainActivityTAG", strMsg + "==========");

        // 发空消息
        mHandler.sendEmptyMessage(1001);
    }
}.start();

<font face="黑体" color = red>注意:这里的 get() 请求源码就是 HttpURLConnection 里面的get() 请求。

<font face="黑体">3、Handler 中接收消息并处理

@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case 1001:
                txt.setText(strMsg);
                break;
            }
        }
    }
};

<font face="黑体">这里的1001就是<font color=red> mHandler.sendEmptyMessage(1001) </font>里面发送过来的那个1001。

2、Message

<font face="黑体"> Message 是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。常用的属性有:

  1. <font face="黑体">what 属性 <font face="黑体">用于区分Handler发送消息的不同线程来源
  2. <font face="黑体">arg1 属性 <font face="黑体">子线程向主线程传递的整型数据
  3. <font face="黑体">obj 属性 <font face="黑体">Object

1)、在子线程发送 Message 消息去主线程

<font face="黑体"> 我们可以在 Message 对象中装载一些数据,携带在消息中,发送给需要接收消息的地方。

<font face="黑体">实现效果如下所示:
装载message对象
<font face="黑体">发送消息具体代码如下所示:

new Thread() {
    @Override
    public void run() {
        super.run();

        // what        用于区分Handler发送消息的不同线程来源
        // arg1, arg2  如果子线程需要向主线程传递整型数据, 则可用这些参数
        // obj         Object
        Message msg = new Message();
        msg.what = 1002;
        msg.arg1 = 666;
        msg.arg2 = 2333;
        msg.obj = new Random();  // 这里只是为了演示msg可以发送对象信息

        mHandler.sendMessage(msg);
    }
}.start();

<font face="黑体">接收消息具体代码如下所示:

@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case 1001:
                txt.setText(strMsg);
                break;
            case 1002:
                String str2 = "发送过来的Message数据为:" +"what: " + msg.what + " arg1: " + msg.arg1 + " arg2: " + msg.arg2 +
                        ", 随机数" + ((Random) msg.obj).nextInt();
                Toast.makeText(MainActivity.this, str2, Toast.LENGTH_LONG).show();
                break;
        }
    }
};

3、MessageQueue

<font face="黑体">MessageQueue 就是消息队列的意思,它主要用于存放所有通过 Handler 发送过来的消息。这部分消息会一直存放于消息队列当中,等待被处理。每个线程只会有一个 MessageQueue 对象。我们上面的看到的 handleMessage() 方法其实就是从这个 MessageQueue 里面把方法提取出来去处理。我们在开发的过程中是无法直接的接触 MessageQueue 的,但是它确一直在起作用。

<font face="黑体">问题:为什么 Handler 发送过来的 Message 不能直接在handleMessage() 方法中处理。而是要先存放到 MessageQueue 里面?
<font face="黑体">答:其实原因就是同一个线程一下只能处理一个消息,并不具有并发的功能。所以就先通过队列将所有的消息都保存下来并安排每个消息的处理顺序(队列的特点:先进先出),然后我们的 UI 线程在挨个将它们拿出来处理。

4、Looper

<font face="黑体">Looper 是每个线程中 MessageQueue 的管家,调用 Looper 的 loop() 方法,就会进入到一个无限循环当中,然后每当 MessageQueue 中存在一条消息,Looper 就会将这条消息取出,并将它传递到 Handler 的 handleMessage() 方法中。每个线程只有一个 Looper 对象,而且仅对应一个消息队列。那么 Looper 到底是怎么工作的,接下来我们就要通过代码来实现一下 Looper 的用法。

<font face="黑体">上面我们都是在子线程中发送消息到主线程去处理,那么反过来可以吗?就是在主线程发送消息到子线程去处理,答案是可以的。我们用代码来实现一下,具体步骤如下:

<font face="黑体">1、首先我们需要在子线程中定义一个 Handler 来接收消息

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    private Handler mHandler2;
    
    new Thread() {
        @SuppressLint("HandlerLeak")
        @Override
        public void run() {
            super.run();
            
            mHandler2 = new Handler() {
                @Override
                public void handleMessage(@NonNull Message msg) {
                    super.handleMessage(msg);
                    Log.e("MainActivityTAG", "由主线程传递过来的Message,它的what是:" + msg.what);
                }
            };
        }
    }.start();
}

<font face="黑体">2、然后由主线程发送消息

case R.id.btn3:
    mHandler2.sendEmptyMessage(10000);
    break;

<font face="黑体">我们现在来回顾一下这个流程,首先我们点击按钮的时候调用 sendEmptyMessage() 方法,这个时候这个消息就进入到 MessageQueue 里面,然后由 Looper 读出这一个消息并交由 handleMessage() 这个方法来处理,所以 MessageQueue 和 Looper 都是在幕后工作的。

<font face="黑体">首先可以肯定的是主线程一定有自己的 Looper,那么子线程是否有它自己的 Looper呢?要回答这个问题,我们先来运行一下上面的代码,看看能不能成功的实现由主线程向子线程发送消息。

<font face="黑体">运行结果如下:
错误
<font face="黑体">运行上述代码报了一个<font color = red> "Can't create handler inside thread Thread[Thread-6,5,main] that has not called Looper.prepare()"</font> 的错误。意思就是不能再还没有调用 <font color = red>Looper.prepare() </font>的线程里面去创建 Handler。那么为什么主线程中我们没有调用这个方法不会报错呢?其实答案就是系统会自动的为主线程创建 <font color = red>Looper.prepare() </font>这个方法。那么我们就手动的为子线程创建一下这个方法。

<font face="黑体">添加上<font color = red> Looper.prepare() </font>这个方法之后,我们上述的代码就不会报错了。但是这个时候我们还是无法在子线程接收到消息。原因就是在子线程中我们不仅要手动加上 <font color = red> Looper.prepare() </font>这个方法,还要加上<font color = red> Looper.loop() </font>这个方法。

<font face="黑体"><font color = red> Looper.prepare() </font>这个方法的意思是准备开始一个循环,而<font color = red> Looper.loop() </font>这个方法才是真正的循环,作用就是使得 handleMessage() 一直处于等待状态,不会被结束掉。

<font face="黑体">我们来看下代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    private Handler mHandler2;
    
    new Thread() {
        @SuppressLint("HandlerLeak")
        @Override
        public void run() {
            super.run();
            
            Looper.prepare(); // 准备, 开启一个消息循环. 系统会自动为主线程开启消息循环
            mHandler2 = new Handler() {
                @Override
                public void handleMessage(@NonNull Message msg) {
                    super.handleMessage(msg);
                    Log.e("MainActivityTAG", "由主线程传递过来的Message,它的what是:" + msg.what);
                }
            };
            Looper.loop(); // 循环. 相当于产生了一个while(true){...}
        }
    }.start();
}

<font face="黑体">现在我们来看一下运行结果:
主线程发送消息到子线程
现在我们就已经成功的实现了由主线程发送消息到子线程。

三、运行机制

<font face="黑体">以上呢我们已经将 Handler 机制里面所涉及的四个类都讲完了。现在我们来聊一下它的运行机制。

<font face="黑体">下面是我从网上找到的一张 Handler 运行机制图片,我们就看着这张图片再来讲一下 Handler 的运行机制。

百度图片
<font face="黑体">首先我们来看下左边这一部分,一个 LooperThread 线程,这个线程里面有一个 Looper,然后 Looper 内部有 MessageQueue 消息队列,这三者是一一对应的。

<font face="黑体">只有主线程才会自动的由 Looper 来管理,而其他线程的话必须要显示的调用 Looper.prepare() 这个方法。同时如果希望子线程处于持续接收消息的状态,我们还需要调用 Looper.loop() 方法使其处于等待的状态。

<font face="黑体">我们创建的 Handler 方法在发送消息的时候其实是将消息发送到 MessageQueue 消息队列中。我们看右边 MessageQueue 中有多待处理的消息。然后通过 Looper 不断的取出消息到对应的 Handler 的 handleMessage() 方法中去处理。这就是整个 Handler 的运行机制。

四、利用 Handler 实现计时器案例

<font face="黑体">先来看一下效果,如下图所示:

计时器案例
<font face="黑体">这个计时器案例主要功能就是按下播放按键的时候,计时器就开始计时工作,按下暂停键计时器就停止工作,并显示当前用时。首先计时功能肯定是在子线程完成的。而计时功能肯定需要改变界面的 UI ,所以这里我们就利用 Handler 来将子线程的信息发送到 UI 线程。

<font face="黑体">实现步骤如下所示:

  1. <font face="黑体">在主线程创建 Handler,并覆写 handleMessage() 方法。

    @SuppressLint("HandlerLeak")
       private Handler mHandler = new Handler() {
       @Override
       public void handleMessage(@NonNull Message msg) {
           super.handleMessage(msg);
       }
       };
  2. <font face="黑体">每隔 1s,时间++,并且发送消息到主线程更新 UI,我们这里先用 Thread.sleep() 这个方法来实现每隔1s,一会我们回去优化这个代码。

    new Thread() {
        @Override
        public void run() {
            super.run();
            int i = 1;
            while (flag) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                Message msg = new Message();
                msg.arg1 = i;
                mHandler.sendMessage(msg);
    
                // 时间 ++
                i++;
            }
        }
    }.start();
  3. <font face="黑体">在主线程中用 00:00 的形式来显示计时时间。

    @SuppressLint("HandlerLeak")
       private Handler mHandler = new Handler() {
       @Override
       public void handleMessage(@NonNull Message msg) {
           super.handleMessage(msg);
           int min = msg.arg1 / 60;
           int sec = msg.arg1 % 60;
           // 00:00
           String time = (min < 10 ? "0" + min : "" + min) + ":" + (sec < 10 ? "0" + sec : "" + sec);
           timer.setText(time);
    
           if (!flag) {
               title.setText("计时器");
               state.setImageResource(R.mipmap.start);
               txt.setText("用时: " + time);
           }
       }
       };

<font face="黑体">完整代码如下所示:

public class TimerActivity extends AppCompatActivity {

    private TextView title, timer, txt;
    private ImageView state;

    private boolean flag = false;  // 1: 用于区别当前对按钮的点击是属于开启计时器还是停止计时器  2: 控制while循环

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            int min = msg.arg1 / 60;
            int sec = msg.arg1 % 60;
            // 00:00
            String time = (min < 10 ? "0" + min : "" + min) + ":" + (sec < 10 ? "0" + sec : "" + sec);
            timer.setText(time);

            if (!flag) {
                title.setText("计时器");
                state.setImageResource(R.mipmap.start);
                txt.setText("用时: " + time);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_timer);

        title = findViewById(R.id.title);
        timer = findViewById(R.id.timer);
        txt = findViewById(R.id.txt);

        state = findViewById(R.id.state);
        state.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                if (!flag) {
                    // 本来是禁止状态, 即将开始计时
                    flag = true;
                    title.setText("工作中");
                    state.setImageResource(R.mipmap.stop);
                    txt.setText("");
                    new Thread() {
                        @Override
                        public void run() {
                            super.run();
                            int i = 1;
                            while (flag) {
                                try {
                                    sleep(1000);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }

                                Message msg = new Message();
                                msg.arg1 = i;
                                mHandler.sendMessage(msg);

                                // 时间 ++
                                i++;
                            }
                        }
                    }.start();
                } else {
                    flag = false;
                }
            }
        });
    }
}

<font face="黑体" color = red>注意:上面 flag 标志位有两个作用:

  1. <font face="黑体">用于区别当前对按钮的点击是属于开启计时器还是停止计时器;
  2. <font face="黑体">控制 while 循环。

五、利用 Handler 的 postDelayed 方案优化计时器案例

<font face="黑体">其实这个优化方案就是用 Handler.postDelayed() 的方案来代替原来的 Thread.sleep() 方法。

<font face="黑体">我们直接来看完整的代码:

public class TimerActivity2 extends AppCompatActivity {

    private TextView title, timer, txt;
    private ImageView state;

    private boolean flag = false;

    private String time;

    private int i;
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            int min = i / 60;
            int sec = i % 60;
            time = (min < 10 ? "0" + min : "" + min) + ":" + (sec < 10 ? "0" + sec : "" + sec);
            timer.setText(time);
            i++;

            mHandler.postDelayed(runnable, 1000);
        }
    };

    // post postDelay postAtTime
    private Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_timer);

        title = findViewById(R.id.title);
        timer = findViewById(R.id.timer);
        txt = findViewById(R.id.txt);

        state = findViewById(R.id.state);
        state.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!flag) {
                    flag = true;
                    title.setText("工作中");
                    state.setImageResource(R.mipmap.stop);
                    txt.setText("");

                    i = 1;
                    new Thread() {
                        @Override
                        public void run() {
                            super.run();
                            mHandler.postDelayed(runnable, 1000);
                        }
                    }.start();
                } else {
                    flag = false;
                    title.setText("计时器");
                    state.setImageResource(R.mipmap.start);
                    txt.setText("用时: " + time);

                    mHandler.removeCallbacks(runnable);
                }
            }
        });
    }
}

<font face="黑体">上面代码里面其实是利用了递归的思想实现计时原理的,因为我们在外层的 Runnable 对象中又递归调用了 runnable。所以当我们停止计时功能时,需要移除回调操作,就是将我们的 runnable对象移除掉,需要调用 mHandler.removeCallbacks(runnable)。

六、Handler 内存溢出问题

<font face="黑体">可以看到上面我们在用 handler 的时候都加了一个注解 <font color=red> @SuppressLint("HandlerLeak")。</font>其实原因就是 Handler 在Android 中用于消息的发送与异步处理时,常常在 Activity 中作为一个匿名内部类来定义,此时 Handler 会隐式地持有一个外部类对象(通常是一个Activity)的 <font color=red>强引用</font>。当 Activity 已经被用户关闭时,由于 Handler 还持有 Activity 的引用造成 Activity 无法被 GC 回收,从而造成内存泄露。解决办法一般有两种;

  1. <font face="黑体">主动的清除所有的 Message,当 Activity 销毁的时候我们调用 mHandler.removeCallbacksAndMessages(null) 这个方法;
  2. <font face="黑体">利用弱引用来解决这个问题,定义一个 MyHandler 的静态内部类(此时不会持有外部类对象的引用),在构造方法中传入 Activity并对 Activity 对象增加一个弱引用,这样 Activity 被用户关闭之后,即便异步消息还未处理完毕,Activity 也能够被 GC 回收,从而避免了内存泄露。写法如下所示:

    private MyHandler handler = new MyHandler(this);
       static class MyHandler extends Handler {
       WeakReference weakReference;
       public MyHandler(SecondActivity activity) {
           weakReference = new WeakReference(activity);
       }
    
       @Override
       public void handleMessage(Message msg) {
           
       }
       }

七、小结

<font face="黑体">到此为止跟 Handler 有关的问题我们大致上都已经讲完了,并且也实现了一个小案例。我们讲了跟 Handler 有关的几个常用类,然后根据这些常用类理解了 Handler 的运行机制,又利用 Handler 实现了一个计时器,并优化了代码。最后我们又简单的说了一下 Handler 可能会造成内存泄漏的问题。

<font face="黑体">项目源码下载地址

阅读 357

Android技术学习
一个分享Android相关知识的专栏,有时候也会写一些Kotlin相关技术博客。

真正的大师,永远怀着一颗学徒的心。

5 声望
3 粉丝
0 条评论
你知道吗?

真正的大师,永远怀着一颗学徒的心。

5 声望
3 粉丝
宣传栏