Android消息机制详解

1 什么是Android主(UI)线程

1.1 java中的主线程

参考链接:https://www.cnblogs.com/mingfeng002/p/10323668.html

  • Java程序初始类中的main()方法是java程序的起点,任何其他的线程都是由这个初始线程启动的。这个线程就是程序的主线程。
  • 在Thread.java文件头部的说明中,有这样的介绍:Each application has at least one thread running when it is started, the main thread, in the main {@link ThreadGroup}.

1.2 Android的主线程

  • ActivityThread不是一个Thread类,它是Android APP进程的初始类。

  • ActivityThread类的main方法是APP进程的入口。因此调用这个main方法的线程就是“主”线程(这里对应java中Thread.java的介绍),“主”线程的创建是操作系统层面的,其创建的代码对我们是不可见的。

  • Android的UI是线程不安全的,因此为了防止出现UI相关的线程安全问题,Android规定只有主(UI)线程才能修改UI

  • 通常我们说的Android的主(UI)线程就是调用ActivityThread.main方法的线程,Android中的四大组件都是在主线程中运行的。

2 Android的消息机制

handler消息机制经常用于将子线程的数据传递给主线程,但实际上handler的作用不仅仅于此,它可用于一个进程中的多个线程间的通信,也可以用于组件间的通信。

2.1 基本用法

主线程中创建handler对象,重写handleMessage方法,该方法用于消息的处理,会传入一个Message对象。

val handler = object : Handler() {
    override fun handleMessage(msg: Message) {
        when(msg.what){//msg.what->int值,用来标识不同的消息来源
            //对不同的消息来源进行处理...
        }
    }
}

子线程中创建Message对象,并使用handler发送消息

thread { 
    val msg = Message()
    msg.what = 0
    handler.sendMessage(msg)
}

/*
Message是消息机制中线程间传递数据的载体,内部可携带少量数据,Message的其他属性介绍:
Message.what->携带用来标识不同的消息来源的int类型
Message.arg1/Message.arg2->携带两个传递整形数据的int类型
Message.obj->携带一个Object对象
*/

/*
Handler是消息的处理者,用于发送和处理消息。Handler发送消息,经过一系列流程最终会回到Handler的handlerMessage中处理。
handler发送消息的常用方法:
sendMessage(Message)->发送消息
sendMessageDelayed(Message, long);//延时(ms)发送消息
sendMessageAtTime(Message, long);//未来某一时间点发送消息
这些方法的关系之后会结合源码分析。
*/

实际上消息机制中光有Handler和Message是不够的,还需要Looper类和MessageQueue类,只是主线程在程序启动的时候已经帮我们创建好了,因此我们在与主线程通信才会如此顺利。

简单介绍一下这两个类:

  • MessageQueue是消息队列,用于存放Handler发送的消息。

  • Looper是消息机制的动力来源,Looper会通过轮询的的方式不断的从MessageQueue中取出消息。

2.2 结合源码分析消息机制的流程

2.2.1 Looper的创建

别看用法中创建handler对象很顺利,其实创建handler对象必须保证当前线程有Looper对象,否则会抛出异常。

Handler的构造函数

public Handler(@Nullable Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {//默认是false,该标志用来判断是否有内存泄漏的风险
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();//获取当前线程的Looper对象(每个线程最多只有一个Looper对象)
    if (mLooper == null) {//如果当前线程的Looper对象为空,则创建handler会抛出异常
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;//Callback是Handler内部接口,只有一个handlerMessage方法
    mAsynchronous = async;
}

如何获取和创建Looper对象?为什么每个线程都只有一个Looper对象?

Looper.myLooper方法

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

public static @Nullable Looper myLooper() {//静态方法
    return sThreadLocal.get();
}
  • ThreadLocal并不是一个Thread,它是线程间数据隔离的工具类,ThreadLocal为线程提供了存储变量的能力,使得线程有属于自己的存储数据的区域(独立于其他线程)。使用ThreadLocal的get、set和remove方法管理线程的这些局部数据副本。

因此这里的sThreadLocal.get()方法就是为了获取当前线程存储的数据,也就是存的Looper对象。

ThreadLocal.get方法

public T get() {
    Thread t = Thread.currentThread();//获取当前线程
    ThreadLocalMap map = getMap(t);//获取当前线程实际存储数据的数据结构
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;//将返回的value转换成ThreadLocal中的泛型
            return result;
        }
    }
    return setInitialValue();
}

//getMap方法
ThreadLocalMap getMap(Thread t) {
    //每个线程维护了一个ThreadLocalMap
    return t.threadLocals;
}

//setInitialValue方法
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //当前线程没有ThreadLocalMap的话就创建一个
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
  • ThreadLocalMap是ThreadLocal的内部类,它是实际存储数据的数据结构,每个线程都维护了一个ThreadLocalMap。

具体ThreadLocalMap是怎样的数据结构呢?

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {//ThreadLocalMap的内部类,继承了ThreadLocal<?>的弱引用,维护一组键值对。
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    private int size = 0;
    private int threshold; // Default to 0
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }    
}
  • ThreadLocalMap维护了一个Entry数组,Entry是键值对,是ThreadLocal<?>对象(key)和object对象(value)的映射关系,ThreadLocalMap通过对key使用独有和哈希算法运算出索引值,来存储到数组中。value的类型和ThreadLocal相关联。

因此可以看作每个线程维护一个Entry的数组,这个数组的索引就是ThreadLocal<?>对象,由于Looper只有一个静态final的ThreadLocal<Looper> 对象,因此一个线程最多只会存储一个key为ThreadLocal<Looper>对象的值,也就是最多存储一个Looper。(也就是说存Looper的ThreadLocal对象是全局唯一的,这个ThreadLocal对象在Looper中,所有的线程使用的都是这个ThreadLocal对象来存Looper,但存的value是不一样的

那主线程是如何帮我们创建好Looper的呢?其实在ActivityThread类的main方法中,也就是App初始化的时候,就在Android的主线程中帮我们创建好了Looper。

ActivityThread.Main方法

{ 
    ...
    Looper.prepareMainLooper();//为主线程创建好Looper
    
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop(); //启动Looper
}

Looper.prepareMainLooper()方法

public static void prepareMainLooper() {
    prepare(false);//为当前线程(这里即主线程)创建一个Looper
    synchronized (Looper.class) {//同步类锁,保证下列代码同步执行
        if (sMainLooper != null) {//主线程的Looper如果已存在则抛出异常(prepareMainLooper只可能被调用一次)
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();//Looper用sMainLooper保存了Android主线程的Looper对象,
    }
}

可以看到,ActivityThread的main方法调用了Looper.prepareMainLooper()方法,prepareMainLooper中使用Looper.prepare方法为当前线程创建了一个Looper,并存到了Looper的sMainLooper中。因此相当于为ActivityThread的main方法所在线程创建了一个Looper(也就是Android主线程)。

Looper.prepare具体是怎么为当前线程创建Looper的呢?

Looper.prepare方法

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {//quitAllowed用于规定Looper是否能使用quit方法来终止loop方法的轮询
    if (sThreadLocal.get() != null) {//判断当前线程是否已经存有Looper(value),有的话抛出异常
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));//没有的话创建一个Looper对象作为value存入当前线程
}

Looper的构造函数

final MessageQueue mQueue;
final Thread mThread;

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);//初始化MessageQueue
    mThread = Thread.currentThread();//初始化当前线程
}

可以看到,每个Looper对象维护一个MessageQueue消息队列,创建Looper的时候也会初始化一个消息队列。同时Looper对象会绑定当前所在线程(既可以获取当前线程的Looper,也可以通过Looper对象获取其绑定的线程)。

  • 因此一个线程只有一个Looper,也只有一个MessageQueue。

知道了创建handler必须要线程有Looper,还知道了Looper的创建方式,终于可以开始研究消息是在线程中传递的流程了。

2.2.2 Message的创建

首先,要如何创建消息呢?比起Message的无参构造函数,更常用的是Message的obtain静态方法。

public static final Object sPoolSync = new Object();//静态对象锁,用来保证同步访问sPool
private static Message sPool;//Message类维护了一个全局的Message单向链表(每个Message本身有next属性,可以看成是链表中的节点,sPool是这个链表的首结点。sPool是静态的全局消息池用来存放一些创建过的Message对象,避免了频繁创建和回收新的Message对象)

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {//如果链表不为空的话,返回链表的首结点,链表长度-1。
            Message m = sPool;//m指向首结点
            sPool = m.next;//sPool指向首结点的后一个结点
            m.next = null;
            m.flags = 0; 
            sPoolSize--;
            return m;//返回首结点
        }
    }
    return new Message();{//如果链表为空,调用无参构造函数
}
  
//无参构造函数
public Message() {
}

Message还有一系列不同参数的obtain静态构造方法,但这些方法内部都是调用了无参obtain静态方法,这里不再举例。

Message除了用来传值的what、arg1、arg、obj属性以外,还有when、target、callback等属性,在接下来的消息机制流程源码中遇到了再逐一解释。

2.2.3 消息的发送

消息创建完后,会由handler来负责发送消息,如何发送?又发送到哪呢?handler发送消息有很多种方法,先来看看我们最常用的sendMessage方法

Handler.sendMessage方法

//sendMessage方法
public final boolean sendMessage(@NonNull Message msg) {//内部直接调用延时发送消息的方法,只不过延时时长为0
    return sendMessageDelayed(msg, 0);
}

//sendMessageDelayed方法
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {//内部调用未来某时刻发送消息的方法
    if (delayMillis < 0) {//保证延时时长大于等于0
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);//当前系统时间+延时时长 = 未来发送消息的时刻
}

Handler.post方法

public final boolean post(@NonNull Runnable r) {//和sendMessage方法一样,内部调用了sendMessageDelayed
   return  sendMessageDelayed(getPostMessage(r), 0);
}

//getPostMessage方法
private static Message getPostMessage(Runnable r) {//负责将一个Runnable封装成一个Message
    Message m = Message.obtain();
    m.callback = r;//Message中有一个callback属性,是一个Runnable类型。
    return m;
}

结合上面代码,其实无论是sendMessage还是post还是其他发送消息的方法,最终都会回到sendMessageAtTime这个方法

Handler.sendMessageAtTime()方法

//Handler构造函数
public Handler(@Nullable Callback callback, boolean async) {
    ..
    mLooper = Looper.myLooper();//获取并保存当前线程的Looper对象(每个线程最多只有一个Looper对象)
    ...
    mQueue = mLooper.mQueue;//保存Looper对应的消息队列
    ...
}

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;//获取当前handler绑定的消息队列
    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);//传入消息队列、消息和执行时间
}

//enqueueMessage方法
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;//Message中有一个target属性,是一个Handler类型。这里将需要发送的消息绑定到当前handler
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

handler发送消息的时候调用Handler.enqueueMessage方法将消息加入了handler绑定的消息队列,其内部又调用了MessageQueue.enqueueMessage方法。

[站外图片上传中...(image-775dd9-1635679315987)]

因此可以看到:

  • 每个handler在创建时,保存了创建handler所在线程的Looper,也保存了这个Looper的对应的MessageQueue。
  • 然后每次handler发送消息的时候都会调用这个MessageQueue的enqueueMessage方法,将消息存入这个消息队列中

MessageQueue.enqueueMessage方法

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {//如果此消息没有绑定handler则抛出异常(Handler.enqueueMessage方法中会给消息绑定handler)
        throw new IllegalArgumentException("Message must have a target.");
    }

    synchronized (this) {//类锁
        ...
        msg.when = when;//处理消息的时间(不是what哦)
        Message p = mMessages;//获取当期消息队列的首结点
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {//当前消息队列为空或者新加入的消息when小于等于首结点的when时,新消息则加在消息队列最前面。
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {//遍历这个消息队列,找到执行时间比新消息执行时间晚的消息,然后在此之前插入新消息或者找不到则新消息插入在尾部。
            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) {//需要的话,唤醒Looper(详见后文)
            nativeWake(mPtr);
        }
    }
    return true;
}

可以看到MessageQueue维护了一个单向链表,这个链表是按消息的执行时间由早到晚排好序的,mMessages属性指向了该链表的首结点。

存好了消息该如何取出来呢?还记得ActivityThread.Main方法中创建好主线程的Looper后是如何启动的吗?没错就是Looper.loop方法

Looper.loop方法

public static void loop() {
    //首先获取当前线程的Looper,要是还没有创建就抛出异常,所以使用Looper.loop前一定要记得使用Looper.prepare
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    ...
    //获取该Looper管理的MessageQueue    
    final MessageQueue queue = me.mQueue;
    ...
    //开始无限循环,通过轮询从消息队列中取出消息处理
    for (;;) {
        Message msg = queue.next(); //从消息队列中取出消息,可能会阻塞
        if (msg == null) {//一旦返回null,意味着消息MessageQueue.quit被调用,意味着终止轮询(详见后文)
            return;
        }
        ...
        try {
            msg.target.dispatchMessage(msg);//调用消息所绑定的handler来处理消息
            ...
        } 
        ...
        msg.recycleUnchecked();//回收消息对象,存放到Message.sPool这个全局池中(未达到默认数量的话)
    }
}

Handler.dispatchMessage方法

public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {//如果消息的callback(Runnable类型)不为空的话,执行消息带有的回调
        handleCallback(msg);
    } else {
        if (mCallback != null) {//如果消息本身无回调的话,且handler本身有回调的话,执行此回调唯一的handleMessage方法
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        //执行子类重写的handleMessage方法来处理消息
        handleMessage(msg);
    }
}

//handleCallback方法
private static void handleCallback(Message message) {
    message.callback.run();
}

举例:线程2向线程1发送消息的流程简述

①线程1中创建Looper(Looper.prepare)

②线程1中创建handler

  • 想创建handler的线程必须先创建Looper
  • 一个线程最多一个Looper,一个Looper管理一个MessageQueue
  • 创建handler时,handler绑定当前线程的Looper和Looper对应的MessageQueue
  • 一个线程可以创建多个handler

③线程1中启动Looper(Looper.loop)

③线程2中创建Message

④线程2中使用之前在线程1创建的handler发送消息

  • 最终会调用Handler.enqueueMessage方法,这里会为需要发送的消息绑定handler
  • 接着调用handler绑定的MessageQueue的enqueueMessage方法,将消息存入handler所绑定的消息队列中

⑤线程1的Looper.loop中不断从消息队列中取出消息来处理

  • 取出消息:MessageQueue.next()
  • 将消息分发给msg绑定的handler处理:handler.handleMessage()
2.2.4 轮询的退出

好了,传递的消息处理完了,可是子线程的Looper还在轮询呀,怎么终止和释放子线程呢?(结束或者去做别的事)

还记得创建Looper时的参数吗?

public static void prepare() {
    prepare(true);//默认可以使用quit终止
}

private static void prepare(boolean quitAllowed) {//quitAllowed用于规定Looper是否能使用quit方法来终止loop方法的轮询
    ...
    sThreadLocal.set(new Looper(quitAllowed));//没有的话创建一个Looper对象作为value存入当前线程
}

来看看Looper中关于终止轮询的方法:

public void quit() {
    mQueue.quit(false);
}

public void quitSafely() 
    mQueue.quit(true);
}

都是调用了消息队列的quit方法来终止轮询

MessageQueue.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) {
            removeAllFutureMessagesLocked();//只清空未来的消息,保留即将处理的消息(when=当前时间)
        } else {
            removeAllMessagesLocked();//清空所有消息
        }

        nativeWake(mPtr);//调用本地方法(非java)唤醒对应被阻塞的Looper
    }
}

//Looper的构造函数
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);//创建并绑定MessageQueue
    mThread = Thread.currentThread();//Looper绑定创建自己的线程
}

//MessageQueue的构造函数
MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;//Looper的quitAllowed赋给MessageQueue的mQuitAllowed
    mPtr = nativeInit();
}

你可能会问Looper什么时候被阻塞了呢?还记得在Looper.loop取消息的时候吗?

Message msg = queue.next(); //从消息队列中取出消息,可能会阻塞。

MessageQueue.quit中唤醒阻塞Looper就会回到MessageQueue.next()中的nativePollOnce(ptr, nextPollTimeoutMillis);处

MessageQueue.next()

Message next() {
    ...
    int nextPollTimeoutMillis = 0;//距离继续下次轮询的时间
    //nextPollTimeoutMillis=-1:表示一直阻塞
    //nextPollTimeoutMillis=0:表示不需要阻塞
    //nextPollTimeoutMillis>0:表示阻塞的时间
    for (;;) {//开启一个无限循环
        if (nextPollTimeoutMillis != 0) {//不为0说明需要阻塞
            Binder.flushPendingCommands();//阻塞前调用此方法,可以释放一些对象的引用,防止阻塞期间一直持有待处理对象。
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);//调用本地方法,内部会根据nextPollTimeoutMillis的值来阻塞当前线程,这里也是nativeWake(mPtr),即唤醒线程回调的地方。

        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;//获取消息队列的首结点
            if (msg != null && msg.target == null) {
                // 如果是同步屏障则找到下一条异步消息(等会再介绍同步屏障,先不看这句)
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    //消息还没到执行时间,设置新的阻塞时间
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    //消息到执行时间,取出一条消息
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    return msg;//返回要执行的消息
                }
            } else {
                //消息队列为空,设置为一直阻塞
                nextPollTimeoutMillis = -1;
            }

            //如果消息队列正在处于退出状态,返回null,调用dispose();释放该消息队列
            if (mQuitting) {
                dispose();
                return null;
            }
            ...
        }
        ...
    }
}

可以看到,消息队列会一直阻塞直到有消息到达执行时间,才会返回要执行的消息。当返回为null时,意味着要终止轮询了。因此当没有消息要执行的时候,Looper就会被阻塞。

除了退出和消息到执行时间了,还有什么情况会需要唤醒Looper呢?想起来了吗,加入消息的时候MessageQueue.enqueueMessage中也有类似唤醒的操作,没错新消息到了意味着可能需要唤醒Looper了。

2.2.5 同步屏障

其实消息分为同步消息和异步消息,默认是同步消息。Message.setAsynchronous方法可以用来设置消息是同步还是异步的

//Handler.enqueueMessage
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;//绑定到当前handler
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
    if (mAsynchronous) {//根据handler的mAsynchronous来设置消息是同步消息还是异步消息
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}


//Message.setAsynchronous方法
public void setAsynchronous(boolean async) {//
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}

一般默认都是同步消息,之所以叫因为同步消息是这些消息都是按照一定先后顺序一条条取出来执行的。还有一类消息叫做异步消息异步消息是优先级更高,需要在同步消息前优先处理的消息。两者关系就好比Vip观众和普通观众之间的关系。

如何做到优先处理异步消息呢?这就需要开启同步屏障,同步屏障顾名思义,屏蔽同步消息(普通观众边上稍稍),优先处理异步消息(vip大爷来咯)。

调用MessageQueue.postSyncBarrier()方法插入一条用来开启同步屏障的消息

MessageQueue.postSyncBarrier()方法

public int postSyncBarrier() {//插入一条当前时间的同步屏障
    return postSyncBarrier(SystemClock.uptimeMillis());//传入当前时间
}

private int postSyncBarrier(long when) {//插入一条指定时间的同步屏障,返回此同步屏障的标识token
    // 插入一个同步屏障的标志
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();//创建一条消息作为同步屏障
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;//获取消息队列首结点
        //根据when的值,将同步屏障插入到对应位置
        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;
        }
        return token;//返回此同步屏障的标识
    }
}
  • 同步屏障本质是一条消息,target==null是同步屏障的标志when值表示开启同步屏障的时间
  • 一旦开启同步屏障,MessageQueue.next()中只会处理异步消息

什么时候才能处理同步消息呢,那就需要再次关闭同步屏障

MessageQueue.removeSyncBarrier方法

public void removeSyncBarrier(int token) {//传入同步屏障的标识token来关闭指定的同步屏障
    synchronized (this) {
        Message prev = null;
        Message p = mMessages;
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
        }
        if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                    + " barrier token has not been posted or has already been removed.");
        }
        final boolean needWake;
        if (prev != null) {
            prev.next = p.next;
            needWake = false;
        } else {
            mMessages = p.next;
            needWake = mMessages == null || mMessages.target != null;
        }
        p.recycleUnchecked();

        // If the loop is quitting then it is already awake.
        // We can assume mPtr != 0 when mQuitting is false.
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}

日常开发中,很少会用到同步屏障。同步屏障主要在系统源码中,一些负责 UI 更新相关的消息需要优先处理,设为异步消息。比如,在 View 更新时,draw、requestLayout、invalidate 等很多地方都调用了ViewRootImpl#scheduleTraversals(),此方法中用到了同步屏障,这里不再深入。

2.3 消息机制的应用

Android提供了几种扩展方式,内部实现都是基于Handler消息机制

2.3.1 Activity.runOnUiThread(Runnable)
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}
2.3.2 View.post(Runnable)
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}
2.3.3 AsyncTask

AsyncTask封装了Thread和Handler,适用于异步执行后台任务,AsyncTask有三个泛型参数。

泛型参数 用途
Params 后台执行任务时传入的数据类型
Progress 后台任务进度发生变化时传入的数据类型
Result 任务执行完毕后返回值的类型

AsyncTask是一个抽象类,需要重写其中的方法才能完成任务的定制

方法 说明
onPreExecute() 任务执行前调用,用于做一些类似初始化这样的准备工作
doInBackground(Params...) 定义处理任务的方法,返回值为Result泛型。此方法中可以通过 publishProgress(Progress...) 方法来更新任务的进度,publishProgress 会调用 onProgressUpdate 方法。
onProgressUpdate(Progress...) 后台任务的执行进度改变时调用,在主线程中执行,可以进行UI操作。
onPostExecute(Result) 后台任务执行完毕后调用。
  • 使用AsyncTask对象的execute(Params...)方法启动任务。
  • AsyncTask对象必须在 UI 线程中创建,execute方法也必须在UI线程中调用。
  • 不要手动调用 onPreExecute(),doInBackground(),onProgressUpdate(),onPostExecute() 这几个方法。
  • doInBackground() 不在主线程,因此不能进行UI操作。
  • 一个任务实例只能执行一次,如果执行第二次将会抛出异常。
  • execute() 方法会让同一个进程中的 AsyncTask 串行执行,如果需要并行,可以调用 executeOnExcutor 方法。
  • 现在推荐使用kotlin的协程处理异步任务

2.4 使用中可能遇到的问题

2.4.1 handler导致的内存泄漏的问题
  • 非静态内部类是会隐式持有外部类的引用,所以使用非静态内部类或者非静态匿名类的方式创建出的handler会持有外部Activity的引用。

  • 于是当其他线程持有了该Handler,线程一直没被销毁(比如线程池的线程),就意味着Activity会一直无法被回收,造成内存泄漏。

  • 另外当MessageQueue中如果存在未处理完的Message,Message的target持有handler也会导致,对Activity等外部类的引用无法回收,也会造成内存泄漏。

2.4.2 解决handler内存泄漏的方式

①使用静态内部类+弱引用的方式:

private Handler sHandler = new TestHandler(this);

static class TestHandler extends Handler {
    private WeakReference<Activity> mActivity;
    TestHandler(Activity activity) {
        mActivity = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        Activity activity = mActivity.get();
        if (activity != null) {
            //TODO:
        }
    }
}

②在外部类对象被销毁时,将MessageQueue中的消息清空。

@Override
protected void onDestroy() {
    handler.removeCallbacksAndMessages(null);
    super.onDestroy();
}
2.4.3 自己使用ThreadLocal要小心内存泄漏

问题:gc时只会回收ThreadLocal对象(Key,它被设置成弱引用),但不会回收value对象,如果线程一直持续运行(如线程池的线程),就会导致内存泄漏

解决:用完后使用ThreadLocal.remove方法

3 面试常见问答

  1. 一个线程有几个Handler?

    答:任意个。

  2. 一个线程有几个Looper?如何保证?

    答:最多一个。Looper中只提供了一个静态final的ThreadLocal<Looper>作为Key来存looper实例,因此一个线程的ThreadLocalMap中最多只存了一个looper。

  3. Handler内存泄漏原因? 为什么其他的内部类没有说过有这个问题?

    答:msg会持有handler,非静态内部的handler会持有activity(context),activity则持有大量的内存,因此会导致这些内存的泄漏。

  4. 为何主线程可以new Handler?如果想要在子线程中new Handler要做些什么准备?

    答:因为主线程在程序启动的时候,ActivityThread的main方法中调用了Looper.prepareMainLooper方法来为主线程创建了Looper。创建handler必须保证当前线程有Looper。

  5. 子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?

    答:设置nextPollTimeoutMillis为-1,使用本地方法一直阻塞当前线程。防止浪费cpu性能。

  6. 既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?取消息呢

    答:通过synchronize关键字开启MessageQueue的对象锁,由于一个线程只有一个MessageQueue,访问此对象的所有操作都会同步执行。

  7. 我们使用Message时应该如何创建它?

    答:使用Message.obtain方法

  8. Looper死循环为什么不会导致应用卡死

    答:应用卡死通常是指ANR,比如在主线程中做了耗时操作导致的无响应。Looper的循环是Android保证App进程能持续运行的保障,包括Activity的生命周期回调等等操作都是通过主线程的Looper从消息队列中取出消息完成的,一旦消息队列为空,就会调用本地方法使得所在线程阻塞,这样也不会对cpu造成性能浪费。

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

推荐阅读更多精彩内容

  • 一、常见使用场景 消息机制中主要用于多线程的通讯,在 Android 开发中最常见的使用场景是:在子线程做耗时操作...
    zly394阅读 3,839评论 1 12
  • use for 相信于此,绝大多数同学都会回答消息机制是android 为了线程间通信而引入的工具。可以轻松的将一...
    漫步_蜗牛阅读 444评论 0 1
  • 消息机制简介 Handler、Message、MessageQueue、Looper一起实现了android的消息...
    张旭的博客阅读 454评论 0 0
  • Android消息机制 *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布这一定是一个被写烂了的...
    francis_fanfan阅读 311评论 0 2
  • 在Android中,只有主线程才能更新UI,但是主线程不能进行耗时操作,否则会产生ANR异常,所以常常把耗时操作放...
    雷涛赛文阅读 903评论 1 2