面试官带你学Android——面试中Handler 这些必备知识点你都知道吗?

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 机制这里就不进行过多介绍了,大家有兴趣可以参考 segmentfault.com/a/119000000…

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

4. 为什么不用 wait 而用 epoll 呢?

说起来 java 中的 wait / notify 也能实现阻塞等待消息的功能,在 Android 2.2 及以前,也确实是这样做的。

可以参考这个 commit www.androidos.net.cn/android/2.1…

那为什么后面要改成使用 epoll 呢?通过看 commit 记录,是需要处理 native 侧的事件,所以只使用 java 的 wait / notify 就不够用了。

具体的改动就是这个 commit android.googlesource.com/platform/fr…

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 android.googlesource.com/platform/fr…

至于 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®, 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。

这里还有一个知识点就是 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 中

文末

面试:如果不准备充分的面试,完全是浪费时间,更是对自己的不负责!

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 14
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值