likes
comments
collection
share

如何应对Android面试官->Handler原理解析,玩转同步屏障

作者站长头像
站长
· 阅读数 9

前言


如何应对Android面试官->Handler原理解析,玩转同步屏障

本章主要进行 Handler 的原理解析,以及一些面试中常见的问题的解答

源码解析


解析之前,先来几个开胃小菜的问题

  1. 线程间如何通讯?
  2. 为什么线程间不会干扰?
  3. 为什么wait/notify用武之地不大?
  4. 系统给每一个应用分配一个虚拟机有什么好处?

我们根据源码来一探究竟;

当我们通过桌面 lanucher 启动一个 app 的时候,都发生了什么呢?

我们通过 launcher(app) 启动另一个 app 的时候, zygote 进程会 fork 一个进程给每一个应用,同时分配一个 jvm,既然是每一个进程都有对应的一个 jvm,而每一个 jvm 的启动都有一个对应的 main 函数,jvm 通过 main 函数来启动,而每一个 app 的 main 函数就在 ActivityThread 中,而每一个 jvm 都是数据隔离,内存隔离的,保证安全的同时还能自己挂掉了不影响其他应用;

我们进入这个 ActivityThread 的 main 方法看下:

如何应对Android面试官->Handler原理解析,玩转同步屏障

核心的两个调用,Looper.prepareMainLooper() 和 Looper.loop() 这两个方法,通过 prepareMainLooper 和 loop 我们的主线程就跑起来了,我们进入 loop 看一下

如何应对Android面试官->Handler原理解析,玩转同步屏障

一个 for(;;) 死循环,开启了我们的 loop;由这个死循环我们可以看到 Android 所有的代码都是在 handler 上运行的;

因为进入了死循环,那么 Android 通过消息的方式,将一个一个的状态分发出去;

如何应对Android面试官->Handler原理解析,玩转同步屏障

当调用到 Looper.loop() 的时候,就会无限循环的从 MessageQueue 中取消息,而如果想停掉 loop 只能通过 从 MessageQueue 中取到一条空消息的时候,才能停掉;

Message msg = me.mQueue.next(); // might block
if (msg == null) {
    // No message indicates that the message queue is quitting.
    return false;
}

那么什么时候才会返回一个为空的 msg 呢?

应用退出,应用退出的时候会发送一个 quit,quit 中具体做了什么,我们后面会讲解到;

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

我们接下来从 handler 的用法开始着手看起;我们在使用 handler 的时候,通过 sendMessage 来发送一条消息,然后在 handleMessage 中接收消息来实现通信,那么 sendMessage 发生了什么?又是怎么调度到 handleMessage 的?我们来一探究竟;

我们进入 Handler 的源码中看下,可以看到所有的 sendMessage 方法最终都会调用到 sendMessageAtTime 方法;

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);
}

这里直接调用了 handler 自己的 enqueueMessage 方法;

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);
}

而 enqueueMessage 方法调用了 MessageQueue 的 enqueueMessage 方法,最终将消息打到了 MessageQueue 中,将消息放到了消息队列;这里是消息入队的操作,那么消息又是怎么出队列的呢?

所以我们可以在 MessageQueue 中看到有一个 next 函数,从 MessageQueue 中取消息,返回的是一个 Message 对象;

Message next() {
    // 省略部分代码
    
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // Next message is not ready.  Set a timeout to wake up when it is ready.
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    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 {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }
        // 省略部分代码
        // ...   
    }
}

那么,谁来从 MessageQueue 中取消息呢?当然是 Looper 了;Looper 中的 loop 方法,它会调用 MessageQueue 的 next 方法;

public static void loop() {
    // 省略部分代码
    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}

在 loopOnce 中调用 next 方法

private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    // 从 MessageQueue 中取消息    
    Message msg = me.mQueue.next(); // might block
}

取出消息之后,再调用 message 的 target 的 dispatchMessage 方法

try {
    msg.target.dispatchMessage(msg);
    if (observer != null) {
        observer.messageDispatched(token, msg);
    }
    dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
}

而 msg.target 就是 handler,而 handler 的 dispatchMessage 最终调用到 handleMessage;

public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

所以整体链路就是:handler.sendMessage() -> MessageQueue.enqueueMessage() //消息队列插入到节点,Looper.loop() -> MessageQueue.next() -> handler.handleMessage();

在整个过程中,一直操作的就是这个 Message,不管我们是通过 new Message() 来创建还是通过 handler.obtainMessage() 来创建,它对应都是一块内存,这样就形成了一块内存共享,它是从子线程到主线程的一个内存共享;

正是因为内存不分线程,所以这块内存,子线程可以使用,主线程同样可以使用;

所以子线程和主线程是怎么完成切换的?就是通过内存共享,子线程去创建了一个 Message 并通过 handler.sendMessage() 发送到 MessageQueue 中,主线程中从MessageQueue 中获取到这个 Message 对象,这样就完成了线程切换;

MessageQueue

消息队列,它是一个由单链表实现的优先级队列;

单向链表


我们来看下这个 MessageQueue 的 enqueueMessage 方法;

boolean enqueueMessage(Message msg, long when) {
    // 省略部分代码
    Message p = mMessages;
    boolean needWake;
    if (p == null || when == 0 || when < p.when) {
        // New head, wake up the event queue if blocked.
        msg.next = p;
        mMessages = msg;
        needWake = mBlocked;
    }
}

Message p = mMessages;可以看到 MessageQueue 持有着一个 Message,我们来看下这个 Message 的构成;

public final class Message implements Parcelable {
    // 省略部分代码
    Message next;

}

可以看到 Message 中持有着一个 Message 类型的 next,整体结构就是:

MessageQueue -> Message mMessages -> Message next -> Message next ....

这样就形成了一个单向链表;

优先级队列


那么 MessageQueue 是怎么体现出来优先级队列的呢?我们来一探究竟;

handler 在 sendMessage 的时候,是可以传递一个时间的,我们还是通过 enqueueMessage 来看下 message 不为空的时候,是如何排序的

boolean enqueueMessage(Message msg, long when) {
    // 省略部分代码
   
    if (p == null || when == 0 || when < p.when) {
        // New head, wake up the event queue if blocked.
        msg.next = p;
        mMessages = msg;
        needWake = mBlocked;
    } else {
        // Inserted within the middle of the queue.  Usually we don't have to wake
        // up the event queue unless there is a barrier at the head of the queue
        // and the message is the earliest asynchronous message in the queue.
        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; // invariant: p == prev.next
        prev.next = msg;
    }
}

可以看到一个 for 循环,break 之后,插入一条消息,那么这个 for 循环是怎么执行的呢?我们来分析一下;

if (p == null || when < p.when) {
   break;
}

when 就是我要执行的这个 message 的时间,p.when 的 p 就是我正在轮训的这个 message,这两个 message 的时间进行比较,如果要执行的这个 when 小于当前轮训的这个 message 的 when(时间小说明需要越早执行),则终止循环,并将这个要执行的 message 插入到当前轮训的这个 message 的前面;

如何应对Android面试官->Handler原理解析,玩转同步屏障

按照执行时间进行了一个排序;

而队列怎么定义呢? 队列是先进先出,先进我们保证不了了,已经通过时间排序了,我们来看下能不能先出,我们进入 next 函数看下;

Message next() {
    ```
    final long now = SystemClock.uptimeMillis();
    // 省略部分代码
    ```
    if (msg != null) {
        if (now < msg.when) {
            // Next message is not ready.  Set a timeout to wake up when it is ready.
            nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
        }
    }

}

可以看到拿当前时间和发送时间进行对比,所以它一直都是获取的队列中的第一个 Message;

所以 MessageQueue 是一个按照时间排序的优先级队列;

如何应对Android面试官->Handler原理解析,玩转同步屏障

Looper

我们接下来看下 Looper 的源码,Looper 的源码核心就在两个地方,一个是 构造方法,一个是 loop 方法;

构造方法


可以看到,Looper 的构造方法是私有的;

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

那么,Looper 是怎么初始化的呢?是由 prepare 来进行初始化的;

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));
}

一个线程只能有一个 ThreadLocal,通过 ThreadLocal 保证一个线程只能有一个 Looper;而 MessageQueue 来自 Looper,所以,一个线程只有一个 MessageQueue,并且 MessageQueue 是 final 的,一旦创建就不会改变了;


final MessageQueue mQueue;

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

/**
 * Return the {@link MessageQueue} object associated with the current
 * thread.  This must be called from a thread running a Looper, or a
 * NullPointerException will be thrown.
 */
public static @NonNull MessageQueue myQueue() {
    return myLooper().mQueue;
}

所以说,不要说 MessageQueue 属于哪个线程,它是一个容器,只能说它是在哪里创建的(在哪个线程创建的);

线程 -> ThreadLocalMap<ThreadLocal, currentThread> Looper -> MessageQueue 这样的一个对应关系;

常见问题


一个线程有几个Handler?

多个 Handler;

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

一个 Looper,通过 ThreadLocal 保证只有一个;

Looper 中声明的 ThreadLcoal 是 static final 的;

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

所以一个Looper中的 ThreadLocal 也是唯一的;

Looper 的 prepare 方法中调用 ThreadLocal 的 set 方法,存储了一个 Looper;

sThreadLocal.set(new Looper(quitAllowed));

二次调用 prepare 的时候,会判读当前的 Threadlcoal是否为空,不为空抛异常,所以保证了一个线程只能有一个 looper;

if (sThreadLocal.get() != null) {
    throw new RuntimeException("Only one Looper may be created per thread");
}

ThreadLocal 的 set 方法中,获取当前线程,通过当前线程获取当前线程持有的 ThreadLocaMap,并将 Looper 存入这个 map 中,key 为 ThreadLocal;

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

ThreadLocal

  1. 线程上下文的存储变量;
  2. 可以看做一个 hashmap,只不过它的 hash 碰撞解决方法同普通的 hashmap不一样;
  3. 1.7hashmap 通过链表解决 hash 碰撞;
  4. 1.8hashmap 通过链表+红黑树解决 hash 碰撞;

MessageQueue

Looper 初始化的时候,会初始化一个 MessageQueue,messagequeue 也是 final 的,一旦被初始化,就不可以改变;

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

ThreadLoaclMap

每一个线程 都有自己的 ThreadLoaclMap

key: 唯一的 ThreadLocal,为什么唯一,因为 ThreadLocal 是声明为了 static final 类型的;

value:Looper;

线程1 -> ThreadLocalMap1 -> <唯一的ThreadLocal,value1> looper1 线程2 -> ThreadLocalMap2 -> <唯一的ThreadLocal,value2> looper2;

Handler 内存泄露?为什么其他的内部类没有说过这个问题?

handler 声明为匿名内部类的时候,会持有外部类的引用;

调用 handler 的 sendMessage 方法,内部最终调用的是enqueueMessage;

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

会将当前 handler 赋值给 message.target,导致 message 持有 Handler,message -> Handler -> Activity 这样的一个持有关系;

消息会被放入消息队列,那么 messagequeue 就会持有这条消息,MessageQueue -> Message -> Handler ->Activity;

运行中的线程不会被回收,如果这个线程 delay 执行的时间不确定,那么消息被执行的时间不确定,有可能1秒后执行,有可能1分钟后执行,也有可能5分钟后执行,一旦五分钟执行的话,那么在这个 MessageQueue 中的五分钟后被执行的消息都会在内存中被MessageQueue持有着。也就是说 message 被持有着,那么 Handler 就会被持有着,导致 Activity 就会被持有着。Activity 就不能被释放;

为何主线程可以 new handler?如果想要在子线程中 new handler(更新 UI) 要做些什么准备?

子线程创建的话需要调用 Looper.prepare() 和 Looper.loop();

子线程更新 UI 也可以,但是只能更新自己创建的 View 换句话说「Android 的 UI 更新被设计成了单线程」子线程更新 UI,需要创建自己的 Looper;

如何应对Android面试官->Handler原理解析,玩转同步屏障

Handler是如何做到线程切换的?

  1. 内存共享,内存不分线程,同样也不分进程(Handler通信实现的核心方案)
  2. 子线程执行的函数,这个函数就在子线程中,所以 handler.sendMessage(msg) 这个函数从子线程中发出去,最终发送到 MessageQueue.enqueueMessage(msg)中,也就是说通过这个方法将 message 这块内存发送到MessageQueue 中了;
  3. 一个线程只有一个Looper,而一个Looper就只管理着一个 MessageQueue;
  4. 我们的Looper中的loop函数在主线程中不断的轮询 MessageQueue,所以从这个 MessageQueue 中取出的消息给到 dispatchMessage 处理,dispatchMessage 调用 handleMessage,而 handleMessage 函数在主线程中,所以这个 message 就在主线程中被处理了;

Handler消息同步屏障原理「保障同步消息优先执行」

消息是根据执行时间进行先后排序,然后消息是保存在消息队列中,因而消息只能从队列的对头取出来,那么需要紧急处理的消息怎么办?

需要紧急处理的消息,打上一个标签:msg.target = null 标记为同步屏障消息;

if (msg != null && msg.target == null) {
    // Stalled by a barrier.  Find the next asynchronous message in the queue.
    do {
       prevMsg = msg;
       msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
}
	

当消息不为空,并且target == null的时候 轮询这个消息队列,找到同步「需要立即执行」的消息;

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

主线程消息队列没有消息之后,主线程就会 sleep 但是之后为什么不会 anr? 因为应用卡死压根跟这个 Looper 没有关系,应用在没有消息需要处理的时候,它是在睡眠,释放线程;卡死是 ANR,而 Looper是睡眠;

anr 触发机制是事件5秒没有响应,或者广播10没有执行,service 20没有执行完毕; anr 异常是在哪里抛出的?点击事件封装成一个message来处理,是在Choreographer中的 doFrame方法中封装成message的,然后执行doCallback进行消息的处理 msg 在 5s 内没有被处理,超时之后,handler.sendMessage 就会发送一条 anr 提醒;

我们使用 message 时应该如何去创建它?

使用 obtain 从消息池中取已经创建不被使用的 Message 防止内存抖动,这是享元设计模式;

如果使用 new 的方式,那么就会频繁创建,频繁回收,造成内存抖动现象;频繁 GC 造成内存抖动,在内存中就会存在大量碎片,不连续,当创建比较占内存的对象(比如 bitmap)就容易造成 OOM;频繁 GC 也容易造成掉帧,界面卡顿;

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

通过 内部加锁,synchronized (this) {} 机制来保证;

针对多个线程往主线程的 MessageQueue 中发消息,那么 this 锁的就是当前主线程的 MessageQueue,又因为一个线程只能有一个 Looper,只能有一个 MessageQueue,所以能控制多线程访问,确保线程安全;

MessageQueue被加锁之后,那么这个对象里面的所有函数,代码块就都会受限;

好了,Handler 解析就到这里吧;

下一章预告


进程通信 Binder 原理解析;

欢迎三连


来到来了,点个关注点个赞吧,你的支持是我最大的动力~~