带你看懂Android消息机制

一、前言

我们知道在Android中经常需要在不同的线程中进行消息传送。当线程A需要线程B来处理数据操作时,在后者处理完成后如何能将结果通知到线程A?这便引出消息机制的概念。其原理是通过一个MessageQueue消息队列来存储其他线程发送的消息,通过本线程的Looper轮询器不断地取消息队列中的消息并通知给当前线程。接下来我们将进入消息机制源码层去探索它的奥秘。

在阅读本篇文章之前,首先建议读者预览一下本文的目录结构,因为本人编写的思路是将消息机制的三大知识点进行分离单独讲解,所以了解清文章的结构能够帮助读者更好的理解本文的知识内容

二、关键知识及流程

本文主要涉及的内容有ThreadLocalLooperMessageQueueHandler消息屏障闲置时间处理等等。在分析源码前先来了解一下消息机制的工作流程。请看图
在这里插入图片描述

1、消息机制流程

  • 线程A中调用Looper.prepare()初始化Looper
  • 创建Handler对象并重写handleMessage()用于处理消息,这里有个优先顺序,在创建Handler之前必须先要初始化Looper,否则会抛出异常
  • 调用Looper.loop()启动循环,对应图for(;;)部分,之后Looper会不断地从Messagequeue队列中读取消息
  • Messagequeue队列存放中以单链表的结构存放着一个个的message消息,mMessage代表着消息队列的头消息
  • 当队列中有消息时,Looper会取出队列中的头消息,假设队列中不存在异步消息,简单引入一点:消息屏障.当有异步消息时会跳过所有的同步消息首先取出异步消息来执行,我们后文中会详细介绍,这里作简单代入
  • Looper成功取出消息后,回调msg.target.dispatchMessage(msg)(target值为发送此条消息的handler对象),将取出的消息发送给handler
  • handler的dispatchMessage()中,调用handleMessage(msg)执行最终的消息消费
  • 在图中线程B充当这消息队列生产者的角色,在某一时间段线程B执行 mhandler.sendMessage(msg)时,会将消息添加到消息队列中,最后有looper取出

大体流程基本就是这样,更多的细节我会在接下来的文章中详细介绍.

注意: 这里需要注意一点,收发消息的线程如果是非主线程,需要手动来创建一个Looper轮询,其次调用Looper.loop()来启动轮询.这里可能会有初学者会问了,在平常操作中经常仅重写Handler#handleMessage(),并未创建过仍可以使用。没错,的确是这样的,大多数情况下,handler的使用是在主线程中进行的,因为handler的主要目的就是为了接收消息,更新UI,同学们都知道UI的更新只能在主线程中进行,所以handler一般是被定义到主线程中执行的,而主线程中默认已经存在一个mainLooper(),也就是说其实源码层已经帮我们做了Looper的初始化操作,所以我们才可以直接使用它.

既然讲到了主线程的UI更新,那就再宽泛的说一下为什么UI的更新只能在主线程中?作为本文的一点小零食知识分享吧

2、主线程更新UI的原因

如果在子线程中也可以更新UI,那会是一个什么样的画面?我们来设想一下这种场景:
假如在主线程中创建出两个子线程用于网络数据请求,如果两个子线程处理数据后同时刷新UI,画面可能会在前一秒界面数据还是一,后一秒界面数据就发生了变化,这仅仅是两个线程,若存在多个线程,那么界面的显示将变得不可控.那加入同步锁呢?加锁本来就是一个很重的操作,同步锁只会是的界面的更新变得低效、复杂。总之若子线程可以访问UI,那么对于UI的刷新将会变得难以控制,界面闪动或延迟。

UI刷新并不是线程安全的,所以为了线程能够稳定展示,提高效率与安全性,规定只能统一在主线程更新UI。在ViewRootImpl中有这么样一个方法

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
//		只有创建视图结构的原始线程才能更新他的视图,而创建此视图的正是主线程
        }
    }

checkThread()方法会检查当前线程是否与mThread线程相等,而mThread在构造方法中赋值,创建视图的正是主线程,凡是更新视图的行为如setText()setWidth()setHeight()都会执行此方法检查线程.所以只能在主线程刷新UI。为什么有时候在子线程中执行setText()时不会报错呢?那是不是代表部分耗时短,轻量的绘制操作可以在主线程进行呢?NO,你可以在调用此方法前延迟一小会时间,照样报不允许子线程更新UI的异常,这种情况是因为此方法的调用早于ViewRootImpl的实例,逃过了它的checkThread(),所以没有异常,原则上还是不允许的
扯了点题外话。我们回到正轨

//  我们以非主线程下使用消息机制,来分析它的原理
		new Thread(() -> {
            Looper.prepare();
            Handler handlers = new Handler(new Handler.Callback() {
                @Override
                public boolean handleMessage(@NonNull Message msg) {
                    Message message = new Message();
                    System.out.println(message.what);
                    return false;
                }
            });
            Looper.loop();
        }, "线程A");

三、Looper

首先我们分析Looper的初始化,Looper相当于一个运输车,它的工作就是在消息队列中有消息的前提下,不断地从队列中拿到消息,运输到Handler中,交给Handler消费

1、Looper初始化
 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));
    }

调用Looper.prepare()后会执行到这里,quitAllowed参数代表循环器是否是可退出的,默认为true.在主线程中创建时为false不可退出,意味着looper会随着进程的销毁而结束,无法通过中间过程停止轮询,大家可能都知道Android主线程其实就维护着一个大的Looper,消息的更新、事件点击等处理都是在这个大的消息机制下进行的,在ActivityThread内的main()方法中会执行Looper.prepareMainLooper()创建属于主线程的Looper,参数为false,与贴出的方法基本相同,只不过是将判断条件换为了if (sMainLooper != null)

这里出现了一个变量sThreadLocal,顺着代码,我们来看ThreadLocal

1.1、ThreadLoacal

在这里插入图片描述
ThreadLocal可以被认为是一个本地变量管理器,其主要功能是为某线程创建一个"本地变量副本"并存储到此线程中,此值只有该线程可以使用,其他线程无法得到,实现了线程间的互不干涉,数据隔离问题.
ThreadLocal中有两个方法:

1.2 ThreadLocal#set()存储本地变量
 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
//当线程首次调用set()时,Thread中的threadLocals还未初始化,所以首先创建实例,再将value值存储为线程本地副本
//之后每次存储时,便会直接将数据存储到此map中
        if (map != null) 
            map.set(this, value);
        else
            createMap(t, value);
    }

ThreadLocalMap为ThreadLocal的内部类,由Thread使用,主要是将value存储的当前的线程中

1.2 ThreadLocal#get()读取本地变量
//获取线程本地副本数据
  public T get() {
        Thread t = Thread.currentThread();  //获取调用get()的线程
        ThreadLocalMap map = getMap(t);  //取到当前线程的threadLocals并将对应数据返回
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
//ThreadLocalMap的构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            //firstKey为调用set()方法的ThreadLocal对象,这里对该值进行hash计算被作为数组的索引
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //以当前的ThreadLocal对象为key,将数据存储
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

根据ThreadLocalMap的构造函数可以看出其内部创建了一个Entry数组,大小默认为16,而每个所谓本地副本变量其实就是一个个Entry对象以key : value形式存储到当前线程中.如果你还是没有理解它的原理,请不要着急,将它运用到消息机制中,自然而然地你就明白了

回到Looper.prepare()中,因为该方法是在线程A下执行的,首先会将线程A中的threadLocals 进行初始化

 	t.threadLocals = new ThreadLocalMap(this, firstValue);

value是一个可中断Looper对象(quitAllowed=true),也就为线程A绑定了一个Looper循环器,若当前线程的的threadLocals不为null,意味着之前已经有过一次looper的存储,那么抛出异常,可以看出每个线程只能对Looper进行一次初始化

2、核心工作looper()方法

我们为什么要称Looper是一个循环器呢?通过looper()方法的分析将为您揭晓答案

 public static void loop() {
        final Looper me = myLooper();
        if (me == null) {   //当前线程没有初始化Looper,返回null
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
  		for (;;) {
            Message msg = queue.next(); // 从消息队列中读取消息
            if (msg == null) {  //当queue的next返回null时,looper就正式结束了
                return;
            }
              try {
             ....
                msg.target.dispatchMessage(msg);
                //Message的target值是发送此消息的Handler,回调这个Handler对象dispatchMessage()最后将执行handleMessage(),我们稍后分析
            } catch (Exception exception) {
                ....
            } finally {
           		....
            }

上述代码中我们可以看出looper使用一个for的无条件循环,不断地执行queue.next()方法获取消息.我们分析下一行msg判null?如果为null直接退出循环?要知道主线程维护的Looper循环也会执行这个方法,一旦队列返回null,将意味着结束.那么我们就需要好奇messageQueue中是如何管理消息的,那么随后我们将揭开MessageQueue的面纱

3、Looper退出quit()和quitSafely()

Looper的退出共有两个方法,其内部调用的都是一个函数 mQueue.quit(),区别在于

  • quilt(): mQueue.quit(false),传递false,将会删除所有的消息,包括延迟消息,非延迟消息,官方解释意思不建议使用
  • quitSafely(): mQueue.quit(true),见名知意,安全退出,但是此方法操作比较能接受,将所有及时(到该执行时间段)的消息全部执行完毕后才会退出,同时删除所有未到时间的延迟消息

执行quit()将不会再接收新的消息。OK,到此Looper这一小节就分析完成了,初始化、轮询、退出作了简单的分析,作为一个搬运工,他的工作也是比较地简单,其实具体的消息管理工作都是在MessageQueue中执行的,所以我们具体来看看MessageQueue都做了哪些工作

四、MessageQueue

MessageQueue在Looper实例化时进行的初始化,本小节我们主要分析MessageQueue的构造方法、enqueueMessage()next()以及quit()四个方法,工作分别是初始化参数、插入消息、得到消息、退出队列。通过分析这些方法我们便能深刻理解它的原理

1、MessageQueue的初始化

MessageQueue的初始化是在Looper的构造方法中执行的,构造方法包含一个boolean类型的形参

	MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
    }
  • quitAllowed: 表示是否可以退出,比如Looper.prepareMainLooper()的实参传递的为false,即不可退出,而平时使用中常常是允许退出
  • mPtr: 一个long类型变量,用于标记当前阻塞队列的状态,当值为0时表示队列退出状态
2、消息插入enqueueMessage()

当调用handler.sendxxx(msg)时候执行的方法,将其他线程要发送的消息添加到消息队列当中

 boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {  
        /*发送消息时,msg必须要有handler的引用,当然后面也有msg.target为空
        的情况,此消息作为一个标识时,target为null,但是添加标识有单独的方
        法,叫做消息屏障,我们之后会讲解到*/
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {  //消息正在被使用  异常
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {  //队列退出时,mQuitting为true
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                msg.recycle();   //消息回收
                return false;  //消息添加失败
            }

            msg.markInUse();  // 更新消息状态
            msg.when = when;  //需要延时多久执行此消息 延时的时间
            Message p = mMessages;  //mMessages为当前消息头,因为队列中的消息是以单链表形式存储,mMessages为头结点
            boolean needWake;   
            //是否需要唤醒当前线程,当队列中无消息处于空闲状态时,
            //会进行阻塞以节省CPU资源,当有消息进来时,唤醒沉睡的线程执行消息
            //p==null只有第一次添加时头结点才会为null
            //when == 0 表示没有延迟时间,需要即刻执行的消息
            //when < p.when如果队头有延时时间且大于当前添加的消息,那么当前消息作为头结点
            if (p == null || when == 0 || when < p.when) {
            	//满足上述其中一个条件 更换新消息为队头
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;   //mBlocked为当前的队列状态,true表示阻塞状态,需要被唤醒
            } else {//都不满足,说明此消息是一个延时消息,且比p的延时时间长
                
                //后两个条件可以看做一个整体,上面说道p.target为null的情况也存在,当调用postSyncBarrier()时会向队列中追加一个消息屏障标识
                needWake = mBlocked && p.target == null && msg.isAsynchronous();  
                Message prev;
                for (;;) {  //此处循环的目的在于将当前消息插入到队列正确的位置
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {  //如果当前线程需要被唤醒,但是当前为异步消息时,取消唤醒
                        needWake = false;
                    }
                }
                // 找到应该存放的位置后,将消息插入队列中
                msg.next = p; 
                prev.next = msg;
            }

            if (needWake) {   //是否需要唤醒线程
                nativeWake(mPtr);  //唤醒 并更新队列状态值
            }
        }
        return true;
    }

消息填加方法做了一下几点:

  • 被加入msg为即时消息或比头结点延时短,放到头结点
  • 根据消息延时消息的时长由短到长的顺序进行连接,按时间延时长短将消息插入链表对应位置
  • 队列处于阻塞状态,在需要的时候唤醒它
  • 在发送消息结束后,会回收msg对象,我们在平时使用过程中也应该注意个这个细节
3、消息屏障

在这里插入图片描述

消息机制中可分为三种消息:普通消息、异步消息、同步屏障
同步屏障可以被看作一个标识,作用是拦截普通消息而使得异步消息的优先执行。通过postSyncBarrier()可以向队列中插入一个消息屏障

 public int postSyncBarrier() {
 //currentTimeMillis() 获取时间为自1970-1-01 到此刻
 // uptimeMillis()为电脑开机到此刻,更加准确,有兴趣单独了解
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

//  when代表该屏障消息延迟时间
    private int postSyncBarrier(long when) {
      
        synchronized (this) {
            final int token = mNextBarrierToken++;
            //注意此消息是新创建的 且没有给msg.target赋值
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            //同样是按时间顺序插入队列中
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { 
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            //最终返回的token为该消息屏障的唯一标识,便于使用后删除
            return token;
        }
    }

我们应该知道Android中生命周期、动画、绘制等都是msg消息,当向主线程发送一条UI绘制消息时,将会被插入到MessageQueue中,此时需要被处理的消息可能有很多,使得绘制消息不能最快处理而导致界面卡顿.为了保证绘制消息能够优先得到执行,同步屏障机制可以跳过这些普通消息优先执行绘制UI的消息.同步屏障就是屏蔽同步消息去执行异步消息的一种机制,所以叫同步屏障.

4、读取消息next()
        final long ptr = mPtr;
        if (ptr == 0) {  //队列退出时ptr会等于0
            return null; 
        }
        
        int nextPollTimeoutMillis = 0;  //消息延迟时间
        
		for (;;) {
         //更新线程状态,nextPollTimeoutMillis有3个值分别决定当前线程的状态
         //-1 阻塞线程,直到有消息时被唤醒
         //0  不阻塞
         //>0 阻塞时间为该值,队列中仅有延时消息时,nextPollTimeoutMillis=msg.when
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;  //msg默认为队头消息
                // 消息屏障的处理,凡target==null为消息屏障
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
               //只要遇到异步消息,结束循环,即取到最近一条的异步消息
                    } while (msg != null && !msg.isAsynchronous()); 
                }
                //若有消息屏障,此时的msg会指向异步消息
                    if (now < msg.when) {  //msg是当前需要被执行的消息,有延迟则阻塞剩余等待时间
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                    //mBlocked表示当前线程阻塞情况 else为消息需要立即执行
                        mBlocked = false;
                        if (prevMsg != null) {  //当msg处于链表中间部分时,执行msg后要连接该节点的前后节点
                            prevMsg.next = msg.next;
                        } else {
                        //消息头指向下一消息
                            mMessages = msg.next;
                        }
                        msg.next = null;
                       
                        msg.markInUse(); //最后返回当前消息
                        return msg;
                    }
                } else {// 若mMessages 返回null,队列中无队列,阻塞线程
                    nextPollTimeoutMillis = -1;
                }
                
                if (mQuitting) {  //关闭队列
                    dispose();
                    return null;
                }
			//....
            }
        }
  • mMessages 为头消息,如果是消息屏障,变量msg会寻找到最近的异步消息返回,并连接上下节点
  • 若为普通消息,返回消息头,其mMessages 指向链表下一个msg节点
  • 若msg消息不为null且是延时消息,线程睡眠
  • 若msg消息为null,阻塞线程
  • 分析looper时我们说过queue队列返回null,就意味着轮询结束,从next()看得出,返回null的情况很少,仅在队列退出状态中,会返回null

next()源码比较长只贴了部分,关于方法后半段是对消息队列的空闲时间的利用,客户端通过添加IdleHandler并重写queueIdle()来利用这些空闲时间执行一些相关操作,在queueIdle()返回false时回调一次会被移除,否则mIdleHandlers中下次空闲继续回调,具体可翻看源码详细了解,这里作简单介绍

5、队列退出quit(
void quit(boolean safe) {
        if (!mQuitAllowed) {  //主线程不允许退出
            throw new IllegalStateException("Main thread not allowed to quit.");
        }

        synchronized (this) {
            if (mQuitting) {   //队列正在退出,则无需操作
                return;
            }
            mQuitting = true;

            if (safe) {   //方法参数我们前面讲过,true为安全退出,仅删除when大于当前时刻的消息,即在执行完所有到时间的消息后,退出
                removeAllFutureMessagesLocked();
            } else {//非安全退出则将删除所有的消息
                removeAllMessagesLocked();
            }
            //唤醒线程
            nativeWake(mPtr);
        }
    }
  • looper.quit()将执行此方法,默认为非安全退出,删除所有的消息,包括到时需要被执行的msg
  • quit()一旦被调用,外部线程便无法再向此队列发送消息
  • 此时next()中的循环还未结束,唤醒线程后,进入下一次循环,非安全情况下,队列中无消息return null,looper退出,反之处理剩下消息后退出
6、总结

不难看出,消息队列是消息机制的仓库,负责消息的增删、线程的睡眠唤醒、消息的类型、队列的退出,其中有关消息屏障是比较难理解的,有兴趣的读者可以多研究研究

五、Handler

1、Handler的初始化

为什么说子线程中创建Handler前必须要先进行Looper的初始化呢?通过它的构造方法你就明白了

public Handler(@Nullable Callback callback, boolean async) {
	mLooper = Looper.myLooper();    //return sThreadLocal.get(); 获取当前线程Looper对象
        if (mLooper == null) { 
         //答案就在这里, 创建Handler时,会判断当前线程下是否有Looper实例,其创建是调用Looper.prepare()添加到ThreadLocal中
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;  //消息队列
        mCallback = callback;  //消息回调
        mAsynchronous = async; //异步消息标记
}
  • 每个线程仅有一个Looper且使用消息机制是,必须提前初始化
  • handler有多个构造函数,读者可自己翻看,还是比较容易理解的
2、消息回调dispatchMessage()

当looper轮询取到消息后通过msg.target.dispatchMessage(msg);便回调此方法

 public void dispatchMessage(@NonNull Message msg) {
 	//此方法判断使用哪个回调接口
        if (msg.callback != null) {  //msg中携带的回调接口
            handleCallback(msg);
        } else {
            if (mCallback != null) {  //初始化handler传递的callback回调接口
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);  //回调重写的handleMessage()
        }
    }

handler有三种回调接口方式:

  • msg.callback在发送消息同时携带回调接口,有消息时回调此接口
  • callback 在初始化Handler时入参的一个回调接口,有消息则回调该接口handleMessage()
  • 若没有传递回调接口,默认回调Handler的handleMessage()方法
3、Handler的消息发送

总得来说,Handler的发送消息有两种发送postxxx()sendxxx()
两者最后都是进入到了sendMessageDelayed()所以区别不是很大

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

而最后的发送消息也非常的简单,若是延迟消息则赋值第二个参数延时时长,计算方式则累加当前系统的时间,反之为0

六、总结

OK,到这里有关Android消息机制我们就讲完,通篇文章讲解了Looper、MessageQueue、Handler的初始化,消息的插入与取出,主线程更新UI的原因(篇幅原因仅做了简单讲解)、ThreadLocal本地变量、以及比较绕脑的消息屏障,看似简单的消息机制其内部也有着你不知道的一面,比如Android主线程对Handler的使用,当然这个信息量是非常庞大的,只能告诉你Android所有的绘制、事件、生命周期等等,都在使用消息机制,本文仅带领读者去分析消息机制源码,更多的应用场景还需读者自己深挖

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

问心彡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值