乐趣区

关于android:Android面试查漏补缺之Handler详解带你全面理解Handler消息机制

在安卓面试中,对于 Handler 的问题是必备的,然而这些对于 Handler 的知识点你都晓得吗?

一、题目档次

  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 吗

二、题目详解

代码剖析基于 Android SDK 28

大家能够先看下面的问题思考一下,如果都分明的话,上面的文章也没必要看了~

1. Handler 的基本原理

对于 Handler 的原理,相比不必多说了,大家都应该晓得,一张图就能够阐明(图片来自网络)。

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 里做了什么事件呢?

    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 中的音讯,进行执行了。

这里个别会引申一个问题,就是主线程中为什么不必手动调用这两个办法呢?置信大家也都明确,就是 ActivityThread.main 中曾经进行了调用。
通过这个问题,又能够引申到 ActivityThread 相干的常识,这里就不细说了。

3. MessageQueue 如何期待音讯

下面说到 Looper.loop 其实就是开始读取 MessageQueue 中的音讯了,那 MessageQueue 中没有音讯的时候,Looper 在做什么呢?咱们晓得是在期待音讯,那是怎么期待的呢?

通过 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 机制这里就不进行过多介绍了,大家有趣味能够参考 https://segmentfault.com/a/1190000003063859

那其实说到这里,又有一个问题,为什么不必 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 过不了这里的查看。

三、总结

一个小小的 Handler,其实能够引申出很多问题,这里这是列举了一些大家可能疏忽的问题,更多的问题就期待大家去摸索了~
这里来总结一下:

1. 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。

最初

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

最近断断续续整顿了一些面试题,目标是想理解一下大厂招聘的技术热点,一直晋升学习,有趣味的敌人【点击我】收费获取哦。

篇幅无限,仅展现局部内容


退出移动版