美文网首页
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