【Android面试查漏补缺】之Handler详解,带你全面理解Handler消息机制

在安卓面试中,关于 Handler 的问题是必备的,可是这些关于 Handler 的知识点你都知道吗?java

1、题目层次

  1. Handler 的基本原理
  2. 子线程中怎么使用 Handler
  3. MessageQueue 获取消息是怎么等待
  4. 为何不用 wait 而用 epoll 呢?
  5. 线程和 Handler Looper MessageQueue 的关系
  6. 多个线程给 MessageQueue 发消息,如何保证线程安全
  7. Handler 消息延迟是怎么处理的
  8. View.post 和 Handler.post 的区别
  9. Handler 致使的内存泄漏
  10. 非 UI 线程真的不能操做 View 吗

2、题目详解

代码分析基于 Android SDK 28android

你们能够先看上面的问题思考一下,若是都清楚的话,下面的文章也不必看了~面试

1. Handler 的基本原理

关于 Handler 的原理,相比不用多说了,你们都应该知道,一张图就能够说明(图片来自网络)。segmentfault

2. 子线程中怎么使用 Handler

除了上面 Handler 的基本原理,子线程中如何使用 Handler 也是一个常见的问题。 子线程中使用 Handler 须要先执行两个操做:Looper.prepare 和 Looper.loop。 为何须要这样作呢?Looper.prepare 和 Looper.loop 都作了什么事情呢? 咱们知道若是在子线程中直接建立一个 Handler 的话,会报以下的错误:安全

"Can't create handler inside thread xxx that has not called Looper.prepare()

咱们能够看一下 Handler 的构造函数,里面会对 Looper 进行判断,若是经过 ThreadLocal 获取的 Looper 为空,则报上面的错误。网络

public Handler(Callback callback, boolean async) {
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
    }

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

那么 Looper.prepare 里作了什么事情呢?app

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 就是建立了 Looper 并设置给 ThreadLocal,这里的一个细节是每一个 Thread 只能有一个 Looper,不然也会抛出异常。 而 Looper.loop 就是开始读取 MessageQueue 中的消息,进行执行了。socket

这里通常会引伸一个问题,就是主线程中为何不用手动调用这两个方法呢?相信你们也都明白,就是 ActivityThread.main 中已经进行了调用。 经过这个问题,又能够引伸到 ActivityThread 相关的知识,这里就不细说了。async

3. MessageQueue 如何等待消息

上面说到 Looper.loop 其实就是开始读取 MessageQueue 中的消息了,那 MessageQueue 中没有消息的时候,Looper 在作什么呢?咱们知道是在等待消息,那是怎么等待的呢?ide

经过 Looper.loop 方法,咱们知道是 MessageQueue.next() 来获取消息的,若是没有消息,那就会阻塞在这里,MessageQueue.next 是怎么等待的呢?

public static void loop() {
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
        }
    }
Message next() {
        for (;;) {
            nativePollOnce(ptr, nextPollTimeoutMillis);
            // ...
        }
    }

在 MessageQueue.next 里调用了 native 方法 nativePollOnce。

// android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    // ...
    mLooper->pollOnce(timeoutMillis);
    // ...
}

// Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    // ...
    result = pollInner(timeoutMillis);
    // ...
}

int Looper::pollInner(int timeoutMillis) {
    // ...
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
}

从上面代码中咱们能够看到,在 native 侧,最终是使用了 epoll_wait 来进行等待的。 这里的 epoll_wait 是 Linux 中 epoll 机制中的一环,关于 epoll 机制这里就不进行过多介绍了,你们有兴趣能够参考 http://www.javashuo.com/article/p-sqagmtjn-x.html

那其实说到这里,又有一个问题,为何不用 java 中的 wait / notify 而是要用 native 的 epoll 机制呢?

4. 为何不用 wait 而用 epoll 呢?

提及来 java 中的 wait / notify 也能实现阻塞等待消息的功能,在 Android 2.2 及之前,也确实是这样作的。 能够参考这个 commit https://www.androidos.net.cn/android/2.1_r2.1p2/xref/frameworks/base/core/java/android/os/MessageQueue.java 那为何后面要改为使用 epoll 呢?经过看 commit 记录,是须要处理 native 侧的事件,因此只使用 java 的 wait / notify 就不够用了。 具体的改动就是这个 commit https://android.googlesource.com/platform/frameworks/base/+/fa9e7c05c7be6891a6cf85a11dc635a6e6853078%5E%21/#F0

Sketch of Native input for MessageQueue / Looper / ViewRoot

MessageQueue now uses a socket for internal signalling, and is prepared
to also handle any number of event input pipes, once the plumbing is
set up with ViewRoot / Looper to tell it about them as appropriate.

Change-Id: If9eda174a6c26887dc51b12b14b390e724e73ab3

不过这里最开始使用的仍是 select,后面才改为 epoll。 具体可见这个 commit https://android.googlesource.com/platform/frameworks/base/+/46b9ac0ae2162309774a7478cd9d4e578747bfc2%5E%21/#F16

至于 select 和 epoll 的区别,这里也不细说了,你们能够在上面的参考文章中一块儿看看。

5. 线程和 Handler Looper MessageQueue 的关系

这里的关系是一个线程对应一个 Looper 对应一个 MessageQueue 对应多个 Handler。

6. 多个线程给 MessageQueue 发消息,如何保证线程安全

既然一个线程对应一个 MessageQueue,那多个线程给 MessageQueue 发消息时是如何保证线程安全的呢? 说来简单,就是加了个锁而已。

// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // ...
    }
}

7. Handler 消息延迟是怎么处理的

Handler 引伸的另外一个问题就是延迟消息在 Handler 中是怎么处理的?定时器仍是其余方法? 这里咱们先从事件发起开始看起:

// Handler.java
public final boolean postDelayed(Runnable r, long delayMillis)
{
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    // 传入的 time 是 uptimeMillis + delayMillis
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    // ...
    return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    // 调用 MessageQueue.enqueueMessage
    return queue.enqueueMessage(msg, uptimeMillis);
}

从上面的代码逻辑来看,Handler post 消息之后,一直调用到 MessageQueue.enqueueMessage 里,其中最重要的一步操做就是传入的时间是 uptimeMillis + delayMillis。

boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // ...
        msg.when = when;
        Message p = mMessages; // 下一条消息
        // 根据 when 进行顺序排序,将消息插入到其中
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 找到 合适的节点
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
            }
            // 插入操做
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        // 唤醒队列进行取消息
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

经过上面代码咱们看到,post 一个延迟消息时,在 MessageQueue 中会根据 when 的时长进行一个顺序排序。 接着咱们再看看怎么使用 when 的。

Message next() {
    // ...
    for (;;) {
        // 经过 epoll_wait 等待消息,等待 nextPollTimeoutMillis 时长
        nativePollOnce(ptr, nextPollTimeoutMillis);

        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) { // 说明须要延迟执行,经过; nativePollOnce 的 timeout 来进行延迟
                    // 获取须要等待执行的时间
                    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;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                        // 当前没有消息要执行,则执行 IdleHandler 中的内容
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // 若是没有 IdleHandler 须要执行,则去等待 消息的执行
                mBlocked = true;
                continue;
            }

            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // 执行 idle handlers 内容
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // release the reference to the handler

            boolean keep = false;
            try {
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }

            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }

        // Reset the idle handler count to 0 so we do not run them again.
        pendingIdleHandlerCount = 0;

        // 若是执行了 idle handlers 的内容,如今消息可能已经到了执行时间,因此这个时候就不等待了,再去检查一下消息是否能够执行, nextPollTimeoutMillis 须要置为 0
        nextPollTimeoutMillis = 0;
    }
}

经过上面的代码分析,咱们知道了执行 Handler.postDelayd 时候,会执行下面几个步骤:

  1. 将咱们传入的延迟时间转化成距离开机时间的毫秒数
  2. MessageQueue 中根据上一步转化的时间进行顺序排序
  3. 在 MessageQueue.next 获取消息时,对比当前时间(now)和第一步转化的时间(when),若是 now < when,则经过 epoll_wait 的 timeout 进行等待
  4. 若是该消息须要等待,会进行 idel handlers 的执行,执行完之后会再去检查此消息是否能够执行

8. View.post 和 Handler.post 的区别

咱们最经常使用的 Handler 功能就是 Handler.post,除此以外,还有 View.post 也常常会用到,那么这两个有什么区别呢? 咱们先看下 View.post 的代码。

// View.java
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;
}

经过代码来看,若是 AttachInfo 不为空,则经过 handler 去执行,若是 handler 为空,则经过 RunQueue 去执行。 那咱们先看看这里的 AttachInfo 是什么。 这个就须要追溯到 ViewRootImpl 的流程里了,咱们先看下面这段代码。

// ViewRootImpl.java
final ViewRootHandler mHandler = new ViewRootHandler();

public ViewRootImpl(Context context, Display display) {
    // ...
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
            context);
}

private void performTraversals() {
    final View host = mView;
    // ...
    if (mFirst) {
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mFirst = false;
    }
    // ...
}

代码写了一些关键部分,在 ViewRootImpl 构造函数里,建立了 mAttachInfo,而后在 performTraversals 里,若是 mFirst 为 true,则调用 host.dispatchAttachedToWindow,这里的 host 就是 DecorView,若是有读者朋友对这里不太清楚,能够看看前面【面试官带你学安卓-从View的绘制流程】提及这篇文章复习一下。

这里还有一个知识点就是 mAttachInfo 中的 mHandler 实际上是 ViewRootImpl 内部的 ViewRootHandler。

而后就调用到了 DecorView.dispatchAttachedToWindow,其实就是 ViewGroup 的 dispatchAttachedToWindow,通常 ViewGroup 中相关的方法,都是去依次调用 child 的对应方法,这个也不例外,依次调用子 View 的 dispatchAttachedToWindow,把 AttachInfo 传进去,在 子 View 中给 mAttachInfo 赋值。

// ViewGroup
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    for (int i = 0; i < transientCount; ++i) {
        View view = mTransientViews.get(i);
        view.dispatchAttachedToWindow(info,
                combineVisibility(visibility, view.getVisibility()));
    }
}

// View
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    // ...
}

看到这里,你们可能忘记咱们开始刚刚要作什么了。

咱们是在看 View.post 的流程,再回顾一下 View.post 的代码:

// View.java
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    getRunQueue().post(action);
    return true;
}

如今咱们知道 attachInfo 是什么了,是 ViewRootImpl 首次触发 performTraversals 传进来的,也就是触发 performTraversals 以后,View.post 都是经过 ViewRootImpl 内部的 Handler 进行处理的。

若是在 performTraversals 以前或者 mAttachInfo 置为空之后进行执行,则经过 RunQueue 进行处理。

那咱们再看看 getRunQueue().post(action); 作了些什么事情。

这里的 RunQueue 实际上是 HandlerActionQueue。

HandlerActionQueue 的代码看一下。

public class HandlerActionQueue {
    public void post(Runnable action) {
        postDelayed(action, 0);
    }

    public void postDelayed(Runnable action, long delayMillis) {
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

        synchronized (this) {
            if (mActions == null) {
                mActions = new HandlerAction[4];
            }
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
            mCount++;
        }
    }

    public void executeActions(Handler handler) {
        synchronized (this) {
            final HandlerAction[] actions = mActions;
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }

            mActions = null;
            mCount = 0;
        }
    }
}

经过上面的代码咱们能够看到,执行 getRunQueue().post(action); 实际上是将代码添加到 mActions 进行保存,而后在 executeActions 的时候进行执行。

executeActions 执行的时机只有一个,就是在 dispatchAttachedToWindow(AttachInfo info, int visibility) 里面调用的。

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
}

看到这里咱们就知道了,View.post 和 Handler.post 的区别就是:

  1. 若是在 performTraversals 前调用 View.post,则会将消息进行保存,以后在 dispatchAttachedToWindow 的时候经过 ViewRootImpl 中的 Handler 进行调用。
  2. 若是在 performTraversals 之后调用 View.post,则直接经过 ViewRootImpl 中的 Handler 进行调用。

这里咱们又能够回答一个问题了,就是为何 View.post 里能够拿到 View 的宽高信息呢? 由于 View.post 的 Runnable 执行的时候,已经执行过 performTraversals 了,也就是 View 的 measure layout draw 方法都执行过了,天然能够获取到 View 的宽高信息了。

9. Handler 致使的内存泄漏

这个问题就是老生常谈了,能够由此再引伸出内存泄漏的知识点,好比:如何排查内存泄漏,如何避免内存泄漏等等。

10. 非 UI 线程真的不能操做 View 吗

咱们使用 Handler 最多的一个场景就是在非主线程经过 Handler 去操做 主线程的 View。 那么非 UI 线程真的不能操做 View 吗? 咱们在执行 UI 操做的时候,都会调用到 ViewRootImpl 里,以 requestLayout 为例,在 requestLayout 里会经过 checkThread 进行线程的检查。

// ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {
    mThread = Thread.currentThread();
}

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

咱们看这里的检查,其实并非检查主线程,是检查 mThread != Thread.currentThread,而 mThread 指的是 ViewRootImpl 建立的线程。 因此非 UI 线程确实不能操做 View,可是检查的是建立的线程是不是当前线程,由于 ViewRootImpl 建立是在主线程建立的,因此在非主线程操做 UI 过不了这里的检查。

3、总结

一个小小的 Handler,其实能够引伸出不少问题,这里这是列举了一些你们可能忽略的问题,更多的问题就等待你们去探索了~ 这里来总结一下:

1. Handler 的基本原理

一张图解释(图片来自网络) handler

2. 子线程中怎么使用 Handler

  1. Looper.prepare 建立 Looper 并添加到 ThreadLocal 中
  2. Looper.loop 启动 Looper 的循环

3. MessageQueue 获取消息是怎么等待

经过 epoll 机制进行等待和唤醒。

4. 为何不用 wait 而用 epoll 呢?

在 Android 2.2 及以前,使用 Java wait / notify 进行等待,在 2.3 之后,使用 epoll 机制,为了能够同时处理 native 侧的消息。

5. 线程和 Handler Looper MessageQueue 的关系

一个线程对应一个 Looper 对应一个 MessageQueue 对应多个 Handler。

6. 多个线程给 MessageQueue 发消息,如何保证线程安全

经过对 MessageQueue 加锁来保证线程安全。

7. Handler 消息延迟是怎么处理的

  1. 将传入的延迟时间转化成距离开机时间的毫秒数
  2. MessageQueue 中根据上一步转化的时间进行顺序排序
  3. 在 MessageQueue.next 获取消息时,对比当前时间(now)和第一步转化的时间(when),若是 now < when,则经过 epoll_wait 的 timeout 进行等待
  4. 若是该消息须要等待,会进行 idel handlers 的执行,执行完之后会再去检查此消息是否能够执行

8. View.post 和 Handler.post 的区别

View.post 最终也是经过 Handler.post 来执行消息的,执行过程以下:

  1. 若是在 performTraversals 前调用 View.post,则会将消息进行保存,以后在 dispatchAttachedToWindow 的时候经过 ViewRootImpl 中的 Handler 进行调用。
  2. 若是在 performTraversals 之后调用 View.post,则直接经过 ViewRootImpl 中的 Handler 进行调用。

9. Handler 致使的内存泄漏

略过不讲~

10. 非 UI 线程真的不能操做 View 吗

不能操做,缘由是 ViewRootImpl 会检查建立 ViewRootImpl 的线程和当前操做的线程是否一致。而 ViewRootImpl 是在主线程建立的,因此非主线程不能操做 View。

最后

职业生涯历来不是百米赛跑,而是马拉松,不断投资本身,得到能够迁移的技能,独立思考的能力,到中后期越是软性的技能越能给你加成,愿诸位工程师可以远离焦虑,活出多彩的人生。

最近断断续续整理了一些面试题,目的是想了解一下大厂招聘的技术热点,不断提高学习,有兴趣的朋友【点击我】免费获取哦。

篇幅有限,仅展现部份内容

相关文章
相关标签/搜索