美文网首页
Android 消息循环机制详解

Android 消息循环机制详解

作者: 愈强 | 来源:发表于2020-07-05 23:43 被阅读0次

    “发送消息-处理消息” 是最基础的消息处理流程。在Android系统中,这一流程是通过Handler+Message进行的。下面介绍一下Handler的基本用法以及其深层的原理。

    基本用法

    创建一个handler,发送与处理消息。

    下面我创建一个handler,同时定义了两个消息,并添加了消息处理逻辑:

    private static final int MSG_SHOW_TOAST = 1;
    private static final int MSG_FINISH = 2;
    
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what) {
                case MSG_SHOW_TOAST:
                    Toast.makeText(MainActivity.this, "收到了消息1", Toast.LENGTH_SHORT).show();
                    break;
                case MSG_FINISH:
                    finish();
                    break;
            }
        }
    };
    

    此时就可以向这个handler发送消息了:

    handler.sendEmptyMessage(MSG_SHOW_TOAST);
    

    发送消息后,会弹出提示“收到了消息1”。

    还可以发送延时消息:

    handler.sendEmptyMessageDelayed(MSG_FINISH, 2000);
    

    发送消息后,等待2秒钟,当前Activity退出(之前我们添加的消息处理代码中,收到MSG_FINISH消息时执行了finish()方法)。

    基本用法很简单,现在我们来探究一下handleMessage方法收到的msg对象与我们发送的消息有什么关系。

    考察sendEmptyMessage的源码如下:

    public final boolean sendEmptyMessage(int what) {
        return sendEmptyMessageDelayed(what, 0);
    }
    public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageDelayed(msg, delayMillis);
    }
    

    在这里看到,sendEmptyMessage方法内部调用的也是sendEmptyMessageDelayed方法,也就是说普通消息是延时为0的延时消息。

    Handler的多个方法都是这样设计的,我们在开发过程中也要有意识的这样设计代码,可以提高代码的复用性、减少书写相似的逻辑。

    sendEmptyMessageDelayed方法内通过Message.obtain()方法拿到了一个Message对象,并将该对象的what数据修改为我们传递的参数。这里的Message对象就是上面handleMessage方法收到的消息对象。通过判断该对象中的what的值,就可以得到具体的消息。

    关于Message.obtain()方法,后面会做更详细的介绍。现在只需要知道该方法获取一个空消息对象。

    更多的消息类型

    上面介绍了如何使用Handler发送简单的消息以及消息是如何接收处理的。接下来看一下Handler还支持什么形式的消息。

    带参数的消息

    考察Message类,可以看到如下的变量定义:

    public class Message {
        public int what;
        public int arg1;
        public int arg2;
        public Object obj;
    }
    

    与之相对的,Handler中定义了如下的方法:

    public class Handler {
        public final Message obtainMessage();
        public final Message obtainMessage(int what);
        public final Message obtainMessage(int what, int arg1, int arg2);
        public final Message obtainMessage(int what,Object obj);
        public final Message obtainMessage(int what, int arg1, int arg2, Object obj);
    
        public final boolean sendMessage(Message msg);
        public final boolean sendMessageDelayed(Message msg, long delayMillis);
    }
    

    这里表明,一个Message对象可以包含两个int数据以及一个Object数据。Message对象可以通过sendMessage发送或通过sendMessageDelayed延时发送。在使用时可以这样做:

    private void sendMessage(){
        Message msg = handler.obtainMessage(MSG_SHOW_TOAST, 5, 6, "我有%d个苹果,%d个梨。");
        handler.sendMessageDelayed(msg, 2000);
    }
    
    private static final int MSG_SHOW_TOAST = 1;
    
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what) {
                case MSG_SHOW_TOAST:
                    String str = String.format((String) msg.obj, msg.arg1, msg.arg2);
                    Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    };
    // 执行结果:2秒后展示Toast,内容为“我有5个苹果,6个梨。”
    

    另外,也可以通过obtainMessage()方法获取一个空的Message对象,然后自由定制其消息内容。

    Runnable消息

    除了Message消息类型,Handler还支持发送Runnable对象作为消息。比如:

    handler.post(new Runnable() {
        @Override
        public void run() {
            Log.i("YQ","来自Runnable的消息");
        }
    });
    

    来看一下这个方法内部是如何实现的:

    public final boolean post(@NonNull Runnable r) {
        return  sendMessageDelayed(getPostMessage(r), 0);
    }
    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
    

    post内调用了getPostMessage方法,将Runnable对象封装成了一个Message对象,后续的流程就和Message的发送一致了。在getPostMessage方法内,将Runnable对象赋值给了Message内的callback变量。这个过程中用到的callback变量是私有的、getPostMessage方法也是私有的,也就是说上层调用者不能自己构造一个带Runnable的Message对象并发送。在另一方面,post所发送的消息在处理的时候是直接调用参数中的Runnable实例的,这里并没有走到Handler类中的handleMessage方法。
    我们来通过源码了解一下其执行过程:

    public class Handler {
        public void dispatchMessage(@NonNull Message msg) {
            if (msg.callback != null) {
                handleCallback(msg);
            } else {
                // 省略了部分代码...
                handleMessage(msg);
            }
        }
        private static void handleCallback(Message message) {
            message.callback.run();
        }
    }
    

    在消息分发方法dispatchMessage中,代码先判断Message对象中是否包含callback对象,如果有,则直接执行其run方法,如果没有则调用handleMessage方法。

    也就是说,带Runnable的消息优先级是高于带what的消息的。为了避免构建了带what与Runnable的消息导致消息传递错误,系统屏蔽了callback变量的访问权限,仅允许通过post方法发送Runnable消息。

    在另一方面,dispatchMessage也是有漏洞的,因为子类可以覆盖该方法并修改消息派发策略。在开发中一定要规避该问题。为了避免手滑导致继承方法错误,可以使用Handler中带Callback接口的构造方法,Handler(Callback callback)。

    dispatchMessage方法作为Handler中关键的消息分发方法,其实应该声明为final的以禁止子类对其进行覆盖。

    指定时间的消息

    上面看到了及时消息与延时消息。Handler还提供了指定时间的消息,这种类型的方法可以指定消息处理的具体时间:

    boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
    boolean sendMessageAtTime(Message msg, long uptimeMillis)
    boolean postAtTime(Runnable r, long uptimeMillis)
    

    而实际上,及时消息和延时消息,最终都是调用的定时消息方法:

    // 前面提到了sendMessage内调用的是sendMessageDelayed,并将delayMillis设为0。
    public final boolean sendMessageDelayed(@android.annotation.NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        // 这里将延时时间加上当前时间得到实际的消息处理时间。
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    

    注意到这里使用的时间时系统启动时间SystemClock.uptimeMillis(),而不是UTC时间。因为用户可以修改UTC时间,从而导致消息时间不准确。

    除此之外,Handler还提供了发送高优先级消息的方法:

    boolean postAtFrontOfQueue(Runnable r)
    boolean sendMessageAtFrontOfQueue(Message msg)
    

    但是强烈建议不要使用这两个方法。按照Api上的注释,只有非常特殊的情况下才能使用,因为这样做可能会引起很多问题。

    消息的判断与移除

    使用下面的方法可以检查当前消息队列中是否包含指定消息,以及删除消息队列中的指定消息。

    boolean hasMessages(int what)
    boolean hasCallbacks(Runnable r)
    
    void removeMessages(int what)
    void removeCallbacks(Runnable r)
    

    消息池

    之前介绍了,在发送自定消息时,需要使用Handler.obtainMessage系列方法获取一个消息实例。为什么不能直接创建呢?我们先来看一下这个方法内是什么:

    public final Message obtainMessage() {
        return Message.obtain(this);
    }
    

    再观察Message.obtain内的代码:

    public static Message obtain(Handler h) {
        Message m = obtain();
        m.target = h;
        return m;
    }
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }
    

    重点是obtain()方法内的内容,可以看到这里有一个链表形式的消息池(sPool),代码优先从池中获取消息,如果池是空的,则新建消息。

    所以很容易理解,使用obtain方法获取消息对象,是为了避免重复创建消息对象,从而避免因频繁的创建销毁Message对象造成的内存抖动现象。

    最后,我们来看一下Message对象是如何回收的:

    // 定义消息池最大容量
    private static final int MAX_POOL_SIZE = 50;
    // 这是Message类内的方法
    void recycleUnchecked() {
        // 第一步 清空消息对象内信息,内容很简单,已省略 。。。
    
        // 将当前消息放入池中
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }
    

    handler与线程

    Looper参数

    上一节我们使用Handler的默认构造方法创建了一个Handler。考察其构造过程,我们可以发现方法内对包含一句对looper变量的赋值操作(为了说明方便,这里省略的很多代码):

    public Handler() {
        // 。。。
        mLooper = Looper.myLooper();
        // 。。。
    }
    

    观察其他的构造方法,可以看到,还有两个构造方法可以接收Looper参数。

    public Handler(Looper looper);
    public Handler(Looper looper, Handler.Callback callback);
    

    看到这里可能会一脸发懵。Looper是哪里来的?为什么要出现在这里?出现在这里有什么用处?和Handler有什么关系?一连串的问题一一冒了出来。

    先从Looper怎么使用入手,了解一下其用法,然后在深究其原理。

    创建一个Looper

    首先,主线程(又叫UI线程,是Android应用启动时的首个线程,也是代码运行所在的默认线程)中已经包含了一个Looper实例。主线程的Looper实例是使用Looper.prepareMainLooper静态方法创建的,获取主线程的Looper可以使用Looper.getMainLooper()静态方法。

        public static void prepareMainLooper() {
            prepare(false);
            synchronized (Looper.class) {
                if (sMainLooper != null) {
                    throw new IllegalStateException("The main Looper has already been prepared.");
                }
                sMainLooper = myLooper();
            }
        }
        public static Looper getMainLooper() {
            synchronized (Looper.class) {
                return sMainLooper;
            }
        }
    

    使用Looper.prepare()方法可以在线程中创建一个而且也只能创建一个Looper实例。来看一下方法内容:

        public static void prepare() {
            prepare(true);
        }
        private static void prepare(boolean quitAllowed) {
            if (sThreadLocal.get() != null) {
                throw new RuntimeException("Only one Looper may be created per thread");
            }
            sThreadLocal.set(new Looper(quitAllowed));
        }
        public static @Nullable Looper myLooper() {
            return sThreadLocal.get();
        }
    

    这里注意以下几点:

    1. 存在 prepare(boolean) ,其中的boolean参数只有在主线程中创建的时候会使用false作为参数。
    2. 新创建的Looper实例放在了ThreadLocal中作为线程局部存储数据进行保存。这保证了数据只能被当前线程读写。
    3. 使用Looper.myLooper可以获取到当前线程的Looper。注意在当前线程未调用prepare前,该方法会返回空。
    4. 主线程Looper实例被保存在sMainLooper静态变量中,在任意位置可以通过getMainLooper静态方法获取。

    更多ThreadLocal相关的的内容请移步百度。

    Looper作为Handler参数有何用途

    上面提到一个线程可以有一个也最多只能由一个Looper实例。那Handler需要这个Looper实例做什么用呢?

    首先通过Handler的构造方法,可以发现Handler里面对于Looper是强要求的,要么由外部传入,要么使用Looper.myLooper()获取当前线程的Looper。如果获取不到,会直接抛出异常:

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    

    异常描述中提示:当前线程没有调用Looper.prepare()方法。

    查看代码,发现代码中获取了Looper实例的mQueue变量:

    mQueue = mLooper.mQueue;
    

    继续看mQueue的使用场景,看到它参与了消息发送的主要逻辑:

    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }
    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
    
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
    

    这里mQueue的类型是MessageQueue,翻译过来就是“消息队列”。Handler在发送消息的时候,实际就是发送到了这个消息队列中了。在另一端MessageQueue收到消息之后,实际上是发送到了Looper所在的线程中了。而相关的消息处理逻辑就是在对应的线程中执行的。

    在之前的例子中,我们在主线程中使用默认构造方法创建了一个Handler实例,该Handler使用了当前线程的Looper对象(也就是MainLooper)。之后使用该Handler实例发送消息时,发送消息时所处的线程无论是主线程还是子线程,消息执行的线程(handleMessage方法执行的线程或Runnable对象run方法执行的线程)都只能是主线程。

    下面介绍一个在子线程中使用Handler的方法。

    使用HandlerThread

    Android系统SDK提供了HandlerThread类,将线程、Looper、Handler结合在了一起,方便开发者进行使用。该类可以新建一个子线程,并通过Handler发送消息的方式,在子线程中处理相关消息逻辑。

    下面是一个简单的使用示例

    // 创建一个HandlerThread对象,参数为线程的名称
    // 要养成为线程合理命名的好习惯,对于后续的开发、调试、查找问题等都有很大的帮助。
    HandlerThread handlerThread = new HandlerThread("TestHandlerThread");
    // 和普通线程一样,需要使用start方法启动线程。
    handlerThread.start();
    
    // 使用handlerThread内的Looper实例创建Handler对象
    Handler handler = new Handler(handlerThread.getLooper(), new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            // 这里的代码是在handlerThread线程内执行的
            return false;
        }
    });
    
    handler.post(new Runnable() {
        @Override
        public void run() {
            // 这里的代码是在handlerThread线程内执行的
        }
    });
    
    // 使用结束后退出线程
    handlerThread.quit();
    

    HandlerThread.getLooper()方法需要等待线程启动后创建Looper实例,所以在实际使用时应该先启动线程,并在之后择机获取Looper创建Handler。

    Looper介绍

    Looper就是循环,在这里特指消息循环。上节提到,一个Looper对应一个线程中,用于向该线程发送消息。这是怎么做到的呢?下面简单介绍一下。

    消息队列

    看下图,任何线程都可以通过Handler发送消息。而Handler最终将消息发送到了消息队列MessageQueue中。Looper从消息队列中等待、获取消息,并将消息发往线程进行处理。

    消息处理循环

    Looper一直在循环查询消息,为什么没有出现cpu耗尽的情况?原因就在于这里的消息队列MessageQueue是个阻塞队列,而这里的情形就是多线程同步问题中典型的“生产者-消费者”问题。

    与消息相关的坑

    Handler的内存泄漏

    在Activity内创建并使用Handler容易引起内存泄漏。内存泄漏的原因可能是因为handler发送了一个延时消息,可能是handler在等待其他线程的处理结果。原因会多种多样,最终因为作为内部类的handler对象持有了Activity的引用,导致Activity内存泄漏。

    解决方法很简单,将Handler定义为静态内部类,并使用弱引用持有Activity实例即可。直接上代码:

    private static class MyHandler extends Handler {
        private Reference<MainActivity> activityReference;
    
        public MyHandler(MainActivity activity) {
            this.activityReference = new WeakReference<>(activity);
        }
    
        @Override
        public void handleMessage(@NonNull Message msg) {
            MainActivity activity = activityReference.get();
            if (activity != null && !activity.isFinishing()) {
                // Activity没有被回收也没有在销毁中,可以做事情了。。。
            }
        }
    }
    

    弱引用对象如果没有其他的强引用位置,将会被GC回收。

    代码中创建内部类形式的Handler时,IDE会直接提示内存泄漏风险。关注IDE提供的代码warning,并按提示进行修改,可以快速提高代码质量,而且能够解决潜在的bug。

    View中的post

    Android提供的View类中包含了post方法,用于向主线程发送及时或延时的Runnable消息。该方法的行为与Handler中的post方法看起来是一致的,而且实际上View的方法内部也确实调用的Handler的中的post方法。但实际上,通过View发送的消息,只有当View在屏幕上显示的时候才会执行。如果View没有在屏幕上显示,那这个消息可能就永远不会被执行了。

    避免通过handler在多个类之间传递消息

    Handler出现的目的就是传递消息。但是作为一种消息传递机制,他的长处在于“在多个线程之间传递消息”,而不是“在类之间传递消息”。

    考虑到Message消息类型的单一性以及内容的任意性,当用他作为多个类之间的消息接口时,会导致代码无法维护。而类之间传递消息应该使用接口(interface)定义,这样才能保证代码的单一性、局部性和可维护性。

    作为开发者,切勿犯懒,更不能认为少定义了一个接口代码就会显得更简洁。

    专业的事情交给专业的工具去做,让手里的工具箱丰富起来,避免手里拿着锤子的时候看什么都是钉子。

    相关知识点

    • 线程
    • 线程局部存储(ThreadLocal)
    • 多线程数据同步
    • “生产者-消费者”问题
    • 弱引用/软引用

    相关文章

      网友评论

          本文标题:Android 消息循环机制详解

          本文链接:https://www.haomeiwen.com/subject/jztdqktx.html