Handler机制实现原理(一)宏观理论分析与Message源码分析

预热

在写这篇文章前我不止一次的问自己,网上分析Handler机制原理的文章那么多,为啥还要画蛇添足啊?不是说前人们写的文章不好,我就是觉得他们写的不细,有些点不讲清楚,逻辑很难通顺的,每次我学个什么东西时遇到这种情况都贼难受。

我们处在这么一个被层层封装的世界里搞开发,被真相所蒙蔽本来就是一件很痛苦的事,如果分享知识的人再搞出了一堆天书般的“经验总结”,这得多打击求学者的心啊!

此篇文章为我【 Android Framework层分析】系列的第一篇。说了这么一大堆就当作个序吧。作为在探索技术之路中饱经风霜中一员,希望我能谨记前人的经验教训,多走点心,思路清晰的分析,尽量写高质量的文章,既对得起自己也对得起他人。

本文所分析的内容大概有以下几个模块:

  • 开发人员最初设计Handler时想要解决什么问题
  • Handler 为我们提供了哪些功能以及如何使用
  • Handler实现原理的理论分析
  • Handler实现原理的源码分析
  • Android UI线程中Handler的特殊操作

文章很长,但思路是循序渐进的,如果你能坚持读完我相信肯定不会让你失望,只要跟着文章的思路走,就不会有需要反复读好几遍加深理解的地方。建议刚读的时候先快速浏览一遍,在脑海中宏观的理清逻辑,有想不通的细节再仔细研究,如有不明白的地方或觉得我分析有误的地方欢迎留言评论。

设计Handler 的初衷

在分析Handler之前,需要先搞清楚两个概念:

  • 同步与异步的区别
  • 线程与多线程的概念

讲道理上述两点中每个拿出来都涉及到很多东西,一方面我学的也不是很深不好意思献丑另一方面这不是本文重点,所以我就当老铁们都知道啦(嘿嘿一笑)。

同步与异步 - 金拱门篇

Java多线程通信

Java中有很多种方法实现线程之间相互通信访问数据,大概先简单的介绍两个典型的,就不上代码了。

  1. 通过synchronized关键字以“上锁”机制实现线程间的通信。多个线程持有同一个对象,他们可以访问同一个共享变量,利用synchronized“上锁”机制,哪个线程拿到了锁,它就可以对共享变量进行修改,从而实现了通信。

  2. 使用Object类的wait/notify机制,执行代码obj.wait();后这个对象obj所在的线程进入阻塞状态,直到其他线程调用了obj.notify();方法后线程才会被唤醒。

Android多线程的特殊性

在上面的两个Java多线程通信的方法中都有一个共同的特点,那就是线程的阻塞。利用synchronized机制拿不到锁的线程需要等拿到锁了才会继续执行操作,obj.wait();需要等obj.notify();才会继续执行操作。

虽然Android系统是由Java封装的,但是由于Android系统的特殊性,Google的开发人员对Android线程的设计进行了改造。他们把启动APP时运行的主线程定义为UI线程

UI线程负责所有你能想到的所有的跟界面相关的操作,例如分发绘制事件,分发交互事件等可多了。由于其特殊性Android系统强制要求以下两点:

  1. 为保持用户界面流畅UI线程不能被阻塞,如果线程阻塞界面会卡死,若干秒后Android系统抛出ANR。

  2. 除UI线程外其他线程不可执行UI操作。

(此处只是简单介绍一下UI线程,后面会有专门一节分析Android UI线程。)

Android 多线程通信

既然UI线程中不能被阻塞,那么查询数据库和访问网络这类的耗时操作肯定就不能在UI线程中执行了,我们就需要单独开个线程操作。

但是除UI线程外其他线程又不可执行UI操作,最后还是要回到UI线程更新UI,这就需要多线程之间的通信。

可Java中线程间通信又都是阻塞式方法,所以传统的Java多线程通信方式在Android中并不适用。

为此Google开发人员就不得不设计一套UI线程与Worker线程通信的方法。既能实现多线程之间的通信,又能完美解决UI线程不能被阻塞的问题。具体方法有以下几类:

  1. view.post(Runnable action)系列,通过View对象引用切换回UI线程。

  2. activity.runOnUiThread(Runnable action),通过Activity对象引用切换回UI线程。

  3. AsyncTask,内部封装了UI线程与Worker线程切换的操作。

  4. Handler,本文的主角,异步消息处理机制,多线程通信。

小结

说到了这里应该大概明白了当初设计Handler的初衷。

由于Android系统的特殊性创造了UI线程

由于UI线程的特殊性创造了若干个UI线程与Worker线程通信的方法

在这若干个线程通信方法中就包含了Handler

Handler就是针对Android系统中与UI线程通信而专门设计的多线程通信机制

Handler 提供的一些方法

Handler API方法相对其他Android API方法来说算少了的,不过要是都拿出挨个介绍还是很多,所以这里给出官方的API文档地址:不用搭梯子就能看的 Android官网 - Handler API

在介绍Handler的消息处理前还有一件事,为了线程传递数据时方便处理,开发人员为Handler专门设计了一个传递消息的载体Message,这样就能让传输数据比较规范化,它有两个十分重要且常用的属性:

  • int waht; 开发者自定义的消息标识,我们可以根据它来区分不同的消息,例如switch(message.what)
  • Object obj; 开发者想要传递的数据,具体什么类型的都可以。

同样,此处就是简单介绍一下Message,后面会详细分析的。

Handler 提供的方法有些我们是用不到的,能用到的方法大体分为发送消息处理消息切换线程三类。

发送消息类方法

1. sendEmptyMessage
boolean sendEmptyMessage (int what)
发送一个只有消息标识waht的空消息。该方法适用于不需要传递具体消息只是单独的发通知时。

2. sendEmptyMessageAtTime
boolean sendEmptyMessageAtTime (int what, long uptimeMillis)
在具体指定的时间uptimeMillis发送一个只有消息标识waht的空消息。uptimeMillis为系统开机到当前的时间(毫秒)。

3. sendEmptyMessageDelayed
boolean sendEmptyMessageDelayed (int what, long delayMillis)
在过了delayMillis毫秒之后发送一个只有消息标识waht的空消息。

4. sendMessage
boolean sendMessage (Message msg)
发送一条消息。

5. sendMessageAtTime
boolean sendMessageAtTime (Message msg, long uptimeMillis)
在具体指定的时间uptimeMillis发送一条消息。uptimeMillis为系统开机到当前的时间(毫秒)。

6. sendMessageDelayed
boolean sendMessageDelayed (Message msg, long sendMessageDelayed )
在过了delayMillis毫秒之后发送一条消息。

处理消息类方法

handleMessage
void handleMessage (Message msg)
负责接受消息,所有发送的消息都会返回该方法,注意!必须Override这个方法才能接收消息。

切换线程类方法

1. post
boolean post (Runnable r)
Runnable r 会运行在handler对象被创建的线程上。当我们在UI线程创建了Hnadler对象,在Worker线程调用handler.post()方法时,Runnable就会运行在UI线程中。

2. postAtTime
boolean postAtTime (Runnable r, long uptimeMillis)
在具体指定的时间uptimeMillisRunnable运行在Handler对象被创建的线程中。

3. postDelayed
boolean postDelayed(Runnable r, long delayMillis)
在具体指定的时间delayMillis之后让Runnable运行在Handler对象被创建的线程中。

使用Handler

在上节方法介绍中出现了XXXAtTime(long uptimeMillis)XXXDelayed(long delayMillis)这两类控制时间的方法,两类方法的时间参数虽然都是毫秒,但是代表的意义却不一样:

  • XXXDelayed(long delayMillis)中的时间参数是指从当前时间开始delayMillis毫秒后
  • XXXAtTime(long uptimeMillis)中的时间参数是指从系统开机算起uptimeMillis毫秒后

利用静态方法SystemClock.uptimeMillis()可以得到从系统开机到现在的毫秒数,所以,下面两个语句执行的时间是相等的:

  • XXXDelayed(1000);
  • XXXAtTime(SystemClock.uptimeMillis() + 1000)

知道了这些后就可以随意使用Handler了。下面是使用Handler的一个小Demo,代码有点长但大部分都是注释,代码共分为三块:

  1. 创建Handler,实现处理消息逻辑
  2. 定义了Worker线程,在Worker线程内部使用Handler提供的方法。
  3. activity.onCreate(Bundle savedInstanceState)初始化控件,启动Worker线程。
public class HandlerActivity extends AppCompatActivity {

    TextView mTextView;

    // 实现Handler
    Handler mHandler = new Handler(){

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){

                case 1:
                    mTextView.setText("接收到一条从Worker Thread线程发来空消息");
                    break;
                case 2:
                    mTextView.setText("接收到一条从Worker Thread线程发来指定时间的空消息");
                    break;
                case 3:
                    mTextView.setText("接收到一条从Worker Thread线程发来指定时间之后的空消息");
                    break;
                case 4:
                    String data = (String) msg.obj;
                    mTextView.setText("接收到一条从Worker Thread线程发来消息,消息中包含数据:"+ data);
                    break;
                case 5:
                    String data1 = (String) msg.obj;
                    mTextView.setText("接收到一条从Worker Thread线程发来指定时间的消息,消息中包含数据:" + data1);
                    break;
                case 6:
                    String data2 = (String) msg.obj;
                    mTextView.setText("接收到一条从Worker Thread线程发来指定时间之后消息,消息中包含数据:" + data2);
                    break;
            }
        }
    };

    // 定义Worker线程
    class WorkerThread extends Thread{

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

            // 在Worker Thread 向UI Thread 发送一条只有 what 的空消息
            mHandler.sendEmptyMessage(1);

            // 在指定的时间Worker Thread 向UI Thread 发送一条只有 what 的空消息
            mHandler.sendEmptyMessageAtTime(2, SystemClock.uptimeMillis() + 1000);

            // 在指定的时间之后Worker Thread 向UI Thread 发送一条只有 what 的空消息
            mHandler.sendEmptyMessageDelayed(3,1000);

            // 创建一个Message
            Message msg1 = new Message();
            msg1.what = 4;
            msg1.obj = "这是一个从Worker Thread发送的普通信息";
            // 在Worker Thread 向UI Thread 发送一条消息
            mHandler.sendMessage(msg1);

            // 创建一个Message
            Message msg2 = new Message();
            msg2.what = 5;
            msg2.obj = "这是一个在指定的时间从Worker Thread发送的信息";
            // 在指定的时间Worker Thread 向UI Thread 发送一条消息
            mHandler.sendMessageAtTime(msg2,SystemClock.uptimeMillis() + 1000);

            // 创建一个Message
            Message msg3 = new Message();
            msg3.what = 6;
            msg3.obj = "这是一个在指定的时间之后从Worker Thread发送的信息";
            // 在指定的时间之后Worker Thread 向UI Thread 发送一条消息
            mHandler.sendMessageDelayed(msg3,1000);

            // post方法可以让Runnable直接运行在UI Thread中
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mTextView.setText("通过post方法,从Worker Thread回到了UI Thread");
                }
            });

            // postAtTime方法可以让Runnable在指定的时间直接运行在UI Thread中
            mHandler.postAtTime(new Runnable() {
                @Override
                public void run() {
                    mTextView.setText("通过postAtTime方法,从Worker Thread回到了UI Thread");
                }
            },SystemClock.uptimeMillis() + 1000);

            // postDelayed方法可以让Runnable在指定的时间之后直接运行在UI Thread中
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mTextView.setText("通过postDelayed方法,从Worker Thread回到了UI Thread");
                }
            },1000);

        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        // 绑定控件
        mTextView = (TextView)findViewById(R.id.tv);
        // 启动worker线程
        new WorkerThread().start();
    }
}

小结

到这里我们已经熟练的掌握了Handler提供的操作,是不是很简单啊?其实Handler中还有一些比较特别方法这里没有介绍,在后面看源码分析实现原理里会介绍。

Message的用法绝不是实例化出来赋值两个属性那么简单,本节只介绍了一点点,同样在分析源码的时候会详细的介绍。

Handler实现原理 - 理论分析

关于源码分析的文章我真是有一肚子吐槽的话想说,总的来说就是介绍源码的作者嗨的不行,而看文章的人一脸懵逼。吸取了前辈们的教训,这里分析源码前先来一波理论上的分析。

线程中接收消息端的特殊性

首先我们得知道理想状态下使用Handler是希望它被实例化在哪个线程,哪个线程就是消息的接收端,虽然在其他线程内发送消息时调用的同样是这个Handler的引用。这没错吧?

两个线程通信
多线程通信

根据上面的结论可以知道Handler接收消息端是线程独立的,不管handler的引用在哪个线程发送消息都会传回自己被实例化的那个线程中。

但显而易见的是Handler不可能是线程独立的,因为它的引用会在别的线程作为消息的发送端,也就是说它本身就是多线程共享的引用,不可能独立存在于某个线程内。

所以!Handler需要一个独立存在于线程内部且私有使用的类帮助它接收消息!这个类就是Looper!

Looper - 线程独立

通过上节分析我们已经知道设计Looper就是为了辅助Handler接收消息且仅独立于线程内部。那如何才能实现线程独立的呢?

好消息是Java早就考虑到了这一点,早在JDK 1.2的版本中就提供ThreadLocal这么一个工具类来帮助开发者实现线程独立。这里简单分析一下ThreadLocal的使用方法,不分析实现原理了。Android官网 - ThreadLocal API

ThreadLocal 支持泛型,用于定义线程私有化变量的类型,实例化对象时可选Override一个初始化方法initialValue(),这个方法的作用就是给你的引用变量赋初始值,如果没有Override这个方法那么默认你的引用变量就是null的:

    //定义一个线程私有的String类型变量
    private static final ThreadLocal<String> local = new ThreadLocal<String>(){
        
        // 设置引用变量的初始化值
        @Override
        protected String initialValue() {
            return super.initialValue();
        }
    };

定义好了ThreadLocal之后还需要了解三个方法:

  • get() 得到你的本地线程引用变量。
  • set(T value)为你的本地线程引用变量赋值。
  • remove() 删除本地线程引用变量。

是不是很简单呢?有了ThreadLocal之后我们只需要把Looper存进去就能实现线程独立了。

private static final ThreadLocal<Looper> mLooper = new ThreadLocal<Looper>();

到这里再梳理一下流程:

  1. Handler 引用可以多线程间共享。
  2. 当Handler对象在其他线程发送消息时,通过Handler的引用找到它所在线程的Looper接收消息。
  3. Looper 负责接收消息再分发给Handler的接收消息方法。
Looper

但是!这样还会有一个问题,如果多个线程同时使用一个Handler发消息,Looper该怎么办?给接收消息的方法上锁吗?显然不能这样做啊!于是就设计了MessageQueue来解决这个问题。

MessageQueue - 多线程同时发消息

为了防止多个线程同时发送消息Looper一下着忙不过来,于是设计一个MessageQueue类以队列的方式保存着待发送的消息,这样Looper就可以一个个的有序的从MessageQueue中取出消息处理了。

既然MessageQueue是为Looper服务的,而Looper又是线程独立的,所以MessageQueue也是线程独立的。

MessageQueue

小结

现在我们已经知道为了完成异步消息功能需要有Handler家族的四位成员共同合作:

  • Handler: 负责发送消息,为开发者提供发送消息与接收消息的方法。
  • Message: 消息载体,负责保存消息具体的数据。
  • MessageQueue:消息队列,以队列形式保存着所有待处理的消息。
  • Looper:消息接受端,负责不断从MessageQueue中取出消息分发给Handler接受消息端。

这四位成员哪个都不是平白无故出现的。因为要规范化消息传递格式而定义了Message;为了实现消息接收端只存在线程内部私有化使用而定义了Looper;为了解决多线程同时发送数据Looper分发消息处理时会产生的问题而设计MessageQueue队列化消息。

到这里你应该知道了Handler家族四位成员各自负责的是什么工作,以及他们自身的特点特殊性,比如Handler是线程间共享的而Looper是线程独立的,MessageQueue跟Looper又是一对一的。

接下来我们就可以开始读源码了!

Message 源码分析

本文出现的源码版本均为Android 7.1.1(Nougat) - API 25 版本。

Message作为消息传递的载体,源码主要分为以下几个部分:

  1. 操作数据相关,类似getter()setter()这种方法还有之前提到过的whatobj这类属性。
  2. 创建与回收对象实例相关,除了用关键字new外,其他得到对象实例的方法。
  3. 其他工具类性质的扩展方法。

Message中的数据属性与方法

首先说一个本篇文章忽略的属性及相关方法:public Messenger replyTo;

为什么要忽略过去呢?因为Messenger类是基于Message上实现进程间通信的类。注意,是进程间通信,不是线程间通信。一方面进程间通信不是本文分析的重点,另一方面进程间通信需要掌握AIDL方面的知识。

接下来就让我们看看Message源码有哪些可供我们使用的属性吧:

  • public int what;:开发者可自定义的消息标识代码,用于区分不同的消息。
  • public int arg1;:如果要传递的消息只有少量的integer型数据,可以使用这个属性。
  • public int arg2;:同上面arg1
  • public Object obj;开发者可自定义类型的传输数据。

上面四个属性作为常用的消息传递的数据载体可直接赋值,例如msg.arg1 = 100;。基本可以满足我们日常开发中简单消息传递。

如果上面几个数据属性不能满足我们的需求,可以使用扩展数据:Bundle来传递(Bundle是啥应该都知道吧?)

    Bundle data;

    // 得到Bundle数据,如果data是空的就new一个
    public Bundle getData() {
        if (data == null) {
            data = new Bundle();
        }
        return data;
    }

    // 得到Bundle数据,如果data是空的就返回 null
    public Bundle peekData() {
        return data;
    }
    // 设置Bundle数据
    public void setData(Bundle data) {
        this.data = data;
    }

这段代码也没什么逻辑好分析的,值得一提就是Bundle data不是public的,所以我们不能直接操作这个属性,需要通过上面三个方法操作数据。使用Bundle数据也非常简单:

    Bundle bundle = new Bundle();
    bundle.putString("String","value");
    bundle.putFloat("float",0.1f);
        
    Message msg = Message.obtain();
    msg.setData(bundle);

创建与回收Message对象的基本方法

先看一下源码中Meesage的构造方法:

    /** Constructor (but the preferred way 
        to get a Message is to call {@link #obtain() Message.obtain()}).
    */
    public Message() {
    }

没错,这货的构造方法里什么也没有,不过它的注释却告诉我们想要得到Message对象首选的方法应该是调用静态方法Message.obtain()。那这个obtain()方法干了什么呢?其实就是内部维持了一个链表形式的Meesage对象缓存池,这样会节省重复实例化对象产生的开销成本。

老样子还是理论分析一波,数据结构中的链表一个单元有两个值,当前单元的值(head)和下一个单元的地址指针(next),如果下一个单元不存在那么next就是null的。

链表结构

所以,想要实现Message对象链表式缓存池就需要额外的两个Message类型的引用headnext,都说了叫缓存池,所以把headpool更合适一点。

有了链表的基础结构我们再想实例化对象的时候就可以先去链表缓存池中看看有没有,有的话直接从缓存池中拿出来用,没有再new一个。

从缓存池中取对象

由于代码多起来逻辑有些复杂,这样不太好分析,所以我在源码中加了许多自己的注释,下面代码看上去很长,其实把注释都去掉后并没有多少。

    // 用于标识当前对象是否存在于缓存池,0代表不在缓存池中
    int flags;

    /** 
     * 这个常量是供上面的 flags 使用的,它表示in use(正在使用)状态
     *
     * 如果Message对象被存入了MessageQueue消息队列排队等待Looper处
     * 理或者被回收到缓存池中等待重复利用时,那么它就是in use(正在使用)状态
     * 
     * 只有在new Message()和Message.obtain()时候才可以清除掉flags上的in use状态
     *
     * 你不可以让一个in use状态的Message对象去传递消息。
     *
     *  1<< 0 还是1,真不知道为啥要这么写,直接写等于1不就得了
     */
    static final int FLAG_IN_USE = 1 << 0;

    /** 静态常量对象,通过synchronized (sPoolSync)让它作为线程并发操作时的锁
     * 确保同一时刻只有一个线程可以访问当前对象的引用
     */
    private static final Object sPoolSync = new Object();
    
    // 当前链表缓存池的入口,装载着缓存池中第一个可用的对象
    private static Message sPool;

    // 链表缓存池中指向下一个对象引用的next指针
    Message next;
    
    // 当前链表缓存池中对象的数量
    private static int sPoolSize = 0;

    /**
     * 从缓存池中拿出来一个Message对象给你
     * 可以让我们在许多情况下避免分配新对象。
     */
    public static Message obtain() {
        // 上锁,这期间只有一个线程可以执行这段代码
        synchronized (sPoolSync) {
            // pool不等于空就说明缓存池中还有可用的对象,直接取出来
            if (sPool != null) {
                // 声明一个Message引用指向缓存池中的pool对象
                Message m = sPool;
                // 让缓存池中pool引用指向它的next引用的对象
                sPool = m.next;
                // 因为该对象已经从缓存池中被取出,所以将next指针置空
                m.next = null;
                // 将从缓存池中取出的对象的flags的in use标识清除掉
                m.flags = 0; 
                // 缓存池中Message对象数量减去一个
                sPoolSize--;
                return m;
            }
        }
        // 如果缓存池中没有可用的对象就new一个吧
        return new Message();
    }

理论上我们希望sPool引用指向了链表缓存池中的第一个对象,让它作为整个缓存池的出入口。所以我们把它设置成static的,这样它就与实例化出来的对象无关,也就是说无论我们在哪个Message对象中进行操作,sPool还是sPool

静态方法obtain()的代码逻辑流程:

先判断缓存池是不是空的:if(sPool != null),如果是空的就直接:return new Message();,不是空的就声明一个引用让它指向缓存池第一个对象:Message m = sPool;,而缓存池的链表头部sPool引用就指向了链表中下一个对象:sPool = m.next;,因为这个时候缓存池中第一个对象已经取出交给了引用Message m,所以需要清除掉这个对象身上的特殊标识,包括缓存池中的next引用和用来标记对象状态的flags值:m.next = null; m.flags = 0;,最后将缓存池中的对象数量减一:sPoolSize--;

逻辑理清了整个流程就显得很简单了,再看看图解逻辑流程:

从缓存池中取出一个对象

分析到这里我们知道了为什么官方推荐我们使用Message.obtain()得到对象了,因为它是在缓存池中取出来重复利用的,但是通过上面代码也看可以看到,只有缓存池里有东西时也就是sPool != null的时候才可以取,Message是怎么把对象回收到缓存池中的呢?

回收Message对象到缓存池的方法

阅读源码后发现有一个public void recycle()方法用于回收Message对象,但是它也牵扯出了一堆其他方法与属性:

    // 缓存池最大存储值
    private static final int MAX_POOL_SIZE = 50;

    // 区分当前Android版本是否大于或者等于LOLLIPOP版本的全局静态变量,默认初始值为true
    private static boolean gCheckRecycle = true;

    /**
     *  用于区分当前Android版本是否大于或者等于LOLLIPOP版本
     *  内部隐藏方法,在APP启动时就会执行该方法,开发者是不可见的
     *  @hide
     */
    public static void updateCheckRecycle(int targetSdkVersion) {
        if (targetSdkVersion < Build.VERSION_CODES.LOLLIPOP) {
            gCheckRecycle = false;
        }
    }

    /**
     * 判断当前对象的flags是否为in-use状态
     */
    boolean isInUse() {
        return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
    }

    /**
     * 调用这个方法后,当前对象就会被回收入缓存池中。
     * 你不能回收一个在MessageQueue排队等待处理或者正在交付给Handler处理的Message对象
     * 说白了就是in-use状态的不可回收
     */
    public void recycle() {
        // 判断当前对象是否为in-use状态
        if (isInUse()) {
            // 如果当前版本大于或者等于LOLLIPOP则抛出异常
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            // 如果当前版本小于LOLLIPOP什么也不干直接结束方法
            return;
        }
        // 回收Message对象
        recycleUnchecked();
    }

    /**
     * 回收一个可能是in use状态的Message对象
     * 在MessageQueue和Looper内部处理排队Message时也会使用这个方法
     */
    void recycleUnchecked() {
        // 将当前Message对象置为in-use状态
        flags = FLAG_IN_USE;

        // 清除当前Message对象的所有数据属性
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        // 上锁
        synchronized (sPoolSync) {
            // 如果当前缓存池对象中的数量小于缓存池最大存储值(50)就存入缓存池中
            if (sPoolSize < MAX_POOL_SIZE) {
                // 存入缓存池
                next = sPool;
                sPool = this;
                // 缓存池数量加1
                sPoolSize++;
            }
        }
    }

上面代码的逻辑很清晰,执行recycle()方法后先判断当前对象是否为in-use状态:if (isInUse()),如果是in-use状态的话当前Android版本是LOLLIPOP(5.0)版本之前直接结束程序,LOLLIPOP及之后版本抛出异常。如果当前对象不是in-use状态,那么就执行recycleUnchecked()方法先将它切换到in-use状态:flags = FLAG_IN_USE;,再把所有的数据属性全部清除,最后把对象存入缓存池链表中。

回收Message对象

为什么要区分Android LOLLIPOP(5.0)前后版本?

源码刚开始就有两个用于区分Android版本的全局属性和方法:

  • private static boolean gCheckRecycle = true;
  • public static void updateCheckRecycle(int targetSdkVersion)

通过查看源码发现Message类在LOLLIPOP版本进行了一次更新也就是我们现在看到的源码,在LOLLIPOP版本之前虽然recycle()方法的注释上同样警告了我们不能回收in-use对象,但是如果你坚持让in-use状态的对象调用recycle()的话也会也会被回收:

    /**
     * Android LOLLIPOP版本源码
     *
     * Return a Message instance to the global pool.  You MUST NOT touch
     * the Message after calling this function -- it has effectively been
     * freed.
     */
    public void recycle() {
        // 清除数据
        clearForRecycle();
        // 存入缓存池
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

所以在LOLLIPOP版本的时候Google进行了改进,强制要求不可以回收in-use状态的对象否则抛出异常,但是为了兼容之前的版本,所以新增加了个内部私有的区分Android版本的方法。

我们需要手动回收吗

现在我们知道了通过执行recycle()方法回收Message对象,但是如果要为每个Message对象都进行手动回收岂不是很麻烦?

庆幸的是开发人员也想到了这一点,从源码中可以看到其实最终真正执行回收操作的调用recycleUnchecked()方法,且注释中告诉我们MessageQueue和Looper内部也会调用该方法执行回收。

这里先说一个结论,MessageQueue和Looper内部分发处理消息时,当它们得知当前这个Message对象已经使用完毕后就会直接调用recycleUnchecked()方法将它回收掉,等分析到MessageQueue和Looper再具体讲这个地方。

所以,如果我们用实例化Message对象是放入Handler中去传消息的,那么我们就不需要手动回收,他们内部自己就回收了。如果我们使用的Message对象跟Handler,Looper,MessageQueue一点交互都没有,那我们就自己去回收。

包含Handler参数的obtain()方法

给Message内部装了一个Handler起了什么作用呢?首先,我们可以通过上面讲解我们可以得出以下已知的结论:

  1. 表面上看我们使用Handler发送消息后,消息直接传回到了Handler内部的handleMessage(Message msg)方法中。
  2. 实际上是先把消息传入了MessageQueue中,Looper再从MessageQueue依次取出消息分发给Handler。
  3. Looper是线程独立的, Looper和MessageQueue是一对一的。

但是,你有没有想过Looper和Handler是不是一对一的?答案当然是否定的,MessageQueue只负责队列消息,Looper只负责取出消息分发。他们的功能很明确而且通用。

所以,无论当前线程有多少个Handler,同样都只有一个Lopper和一个MessageQueue。

一个线程存在多个Handler

既然每个线程只有一个Looper和MessageQueue的话那么Looper分发消息的时候要如何判断当前这个Message是哪个Handler的呢?所以开发人员就给Message内部配置了一个Handler属性,这样Looper分发消息时直接调用Messgae内部的Handler属性就能找到它对应的handleMessage(Message msg)接收消息的方法了。

源码很简单,就是在空参方法obtain()基础上加了个Handler属性,还有它的getter()setter()

    Handler target;

    public static Message obtain(Handler h) {
        Message m = obtain();
        m.target = h;
        return m;
    }

    public void setTarget(Handler target) {
        this.target = target;
    }

    public Handler getTarget() {
        return target;
    }

包含Runnable参数的obtain()方法

跟上面类似,该方法就是在上面基础加了个Runnable参数,源码如下:


    Runnable callback;

    public static Message obtain(Handler h, Runnable callback) {
        Message m = obtain();
        m.target = h;
        m.callback = callback;
        return m;
    }

    public Runnable getCallback() {
        return callback;
    }

这个Runnable的作用是:在Looper分发消息时如果Runnable callback不是空的,那么就不调用Handler的handleMessage(Message msg)方法,直接运行这个Runnable callback。注意,这里的运行是已经回到了Handler被创建的线程上,也就是说Runnable会运行在Handler被创建的线程上。

更多包含参数的obtain()方法

下面这些带参的obtain()方法我相信不用介绍大家也都能看的懂:

    public static Message obtain(Handler h, int what) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        return m;
    }

    public static Message obtain(Handler h, int what, Object obj) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        m.obj = obj;
        return m;
    }

    public static Message obtain(Handler h, int what, int arg1, int arg2) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        m.arg1 = arg1;
        m.arg2 = arg2;
        return m;
    }

    public static Message obtain(Handler h, int what, 
            int arg1, int arg2, Object obj) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        m.arg1 = arg1;
        m.arg2 = arg2;
        m.obj = obj;
        return m;
    }

扩展方法

特殊属性long when;

    long when;

    public long getWhen() {
        return when;
    }

既然这个属性的名字都叫when了那肯定就是跟时间有关了。还记得Handler给我们提供的方法中有几个可以控制时间的方法吗?例如XXXAtTime()XXXDelayed()when这个属性就是就用存储当前这个Message应该被处理的时间。当我们讲Handler和MessageQueue时会在提到它。

序列化对象

Message支持对象的序列化,就是可以把对象转为字节形式,可以保存到本地也可以用于网络传输。如果不了解这方面的知识建议先查阅相关文章。

为了实现对象序列化,我们需要实现Parcelable接口,实例化Parcelable.Creator接口,并重写describeContents()writeToParcel()方法。先看源码,再讲他们都是干啥的。

// 实现Parcelable接口
public final class Message implements Parcelable {

    ...

    // 实例化Parcelable.Creator接口,完成Parcel对象转Message对象的操作
    public static final Parcelable.Creator<Message> CREATOR
            = new Parcelable.Creator<Message>() {

        public Message createFromParcel(Parcel source) {
            Message msg = Message.obtain();
            msg.readFromParcel(source);
            return msg;
        }
        
        public Message[] newArray(int size) {
            return new Message[size];
        }
    };

    // 序列化对象时的特殊种类对象描述,这里开发人员没有修改,就是默认的0
    public int describeContents() {
        return 0;
    }

    // 重写Parcelable接口的writeToParcel方法,将Message对象转为Parcel对象,
    public void writeToParcel(Parcel dest, int flags) {
       
         // 以下代码均是将Message对象中的属性写入Parcel对象中

        if (callback != null) {
            throw new RuntimeException(
                "Can't marshal callbacks across processes.");
        }
        dest.writeInt(what);
        dest.writeInt(arg1);
        dest.writeInt(arg2);
        if (obj != null) {
            try {
                Parcelable p = (Parcelable)obj;
                dest.writeInt(1);
                dest.writeParcelable(p, flags);
            } catch (ClassCastException e) {
                throw new RuntimeException(
                    "Can't marshal non-Parcelable objects across processes.");
            }
        } else {
            dest.writeInt(0);
        }
        dest.writeLong(when);
        dest.writeBundle(data);
        Messenger.writeMessengerOrNullToParcel(replyTo, dest);
        dest.writeInt(sendingUid);
    }
    
    // 从Parcel对象中读取数据转为当前对象的属性
    private void readFromParcel(Parcel source) {
        what = source.readInt();
        arg1 = source.readInt();
        arg2 = source.readInt();
        if (source.readInt() != 0) {
            obj = source.readParcelable(getClass().getClassLoader());
        }
        when = source.readLong();
        data = source.readBundle();
        replyTo = Messenger.readMessengerOrNullFromParcel(source);
        sendingUid = source.readInt();
    }

}

代码看上去很长,其实很简单,就是实现了Parcelable接口,接着重写将Message对象转为Parcel对象的方法writeToParcel(),再重写了接口Parcelable.Creator<T>完成Parcel对象转Message对象的方法。

这里用到的主要都是序列化对象Parcelable接口相关的知识。

设置Message是异步传输还是同步传输

正常情况下,我们的消息其实是同步处理的,为什么这么说呢?

Looper的工作就是把消息队列MessageQueue中的消息依次取出然后分发,每个消息传输都是有时间顺序的,这个动作都是可控制的。

然而,将消息设置成异步传输后那么Message对象将不再受Looper的控制,传输的顺序可能会被打断,不一定哪个消息先传过来。

所以,请谨慎使用异步传输。

    // 该常量代表为异步传输方式
    static final int FLAG_ASYNCHRONOUS = 1 << 1;

    // 判断是否为异步传输
    public boolean isAsynchronous() {
        return (flags & FLAG_ASYNCHRONOUS) != 0;
    }

    // 设置当前对象是否为异步传输
    public void setAsynchronous(boolean async) {
        if (async) {
            flags |= FLAG_ASYNCHRONOUS;
        } else {
            flags &= ~FLAG_ASYNCHRONOUS;
        }
    }

值得一提的是,用于标记是否为异步传输的标识跟用于判断是否为in-use状态的标识是共用的一个属性flags

小结

到这里整个Message源码就已经分析的差不多了,如果你有认真看到这个相信你打开电脑中的Message源码看起来将对其中的代码了如指掌(除了本文没介绍的Messenger相关代码)。

在简单的总结一下Message源码,他们大致分为三类:

利用链表式缓存池避操作对象的相关方法:

  • obtain()方法
  • 其他包含参数的obtain()方法
  • 手动回收对象到缓存池的recycle()方法
  • Looper与MessageQueue内部也会使用的回收对象到缓存池的recycleUnchecked()方法

保存数据的相关属性及方法

  • 消息标识int what
  • 简单整数型属性int arg1int arg2
  • 自定义类型数据Object obj
  • 扩展数据Bundle data

其他功能相关属性及方法

  • 控制消息被处理的时间属性int when
  • 序列化对象相关方法
  • 设置异步传输方法相关方法

未完待续&致谢

由于篇幅有限,所以我计划将Handler机制原理系列分解为3或4章,既方便我管理文章也方便其他人观看。文章写到这里已经用时将近两个星期,我也总算是知道了自己理解和让别人理解的差距,接下来速度应该就会快多了。

此篇文章大部分观点均为我自己分析后得出结论猜想,可能某些地方是错误的,欢迎大家提出自己的看法,我们可以一起讨论,观点成立的话,我会及时更新文章中的错误或需要改进的地方,并在附上参与者主页链接实名感谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容