27道 Handler 经典面试题,你能答出多少

在该方法中加入了同步屏障,后续加入一个异步消息MSG_DO_SCHEDULE_CALLBACK,最后会执行到FrameDisplayEventReceiver,用于申请VSYNC信号。

10、Message消息被分发之后会怎么处理?消息怎么复用的?


再看看loop方法,在消息被分发之后,也就是执行了dispatchMessage方法之后,还偷偷做了一个操作——recycleUnchecked

public static void loop() {

for (;😉 {

Message msg = queue.next(); // might block

try {

msg.target.dispatchMessage(msg);

}

msg.recycleUnchecked();

}

}

//Message.java

private static Message sPool;

private static final int MAX_POOL_SIZE = 50;

void recycleUnchecked() {

flags = FLAG_IN_USE;

what = 0;

arg1 = 0;

arg2 = 0;

obj = null;

replyTo = null;

sendingUid = UID_NONE;

workSourceUid = UID_NONE;

when = 0;

target = null;

callback = null;

data = null;

synchronized (sPoolSync) {

if (sPoolSize < MAX_POOL_SIZE) {

next = sPool;

sPool = this;

sPoolSize++;

}

}

}

recycleUnchecked方法中,释放了所有资源,然后将当前的空消息插入到sPool表头。

这里的sPool就是一个消息对象池,它也是一个链表结构的消息,最大长度为50。

那么Message又是怎么复用的呢?在Message的实例化方法obtain中:

public static Message obtain() {

synchronized (sPoolSync) {

if (sPool != null) {

Message m = sPool;

sPool = m.next;

m.next = null;

m.flags = 0; // clear in-use flag

sPoolSize–;

return m;

}

}

return new Message();

}

直接复用消息池sPool中的第一条消息,然后sPool指向下一个节点,消息池数量减一。

11、Looper是干嘛呢?怎么获取当前线程的Looper?为什么不直接用Map存储线程和对象呢?


在Handler发送消息之后,消息就被存储到MessageQueue中,而Looper就是一个管理消息队列的角色。 Looper会从MessageQueue中不断的查找消息,也就是loop方法,并将消息交回给Handler进行处理。

而Looper的获取就是通过ThreadLocal机制:

static final ThreadLocal sThreadLocal = new ThreadLocal();

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

}

public static @Nullable Looper myLooper() {

return sThreadLocal.get();

}

通过prepare方法创建Looper并且加入到sThreadLocal中,通过myLooper方法从sThreadLocal中获取Looper。

12、ThreadLocal运行机制?这种机制设计的好处?


下面就具体说说ThreadLocal运行机制。

//ThreadLocal.java

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;

return result;

}

}

return setInitialValue();

}

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类中的get和set方法可以大致看出来,有一个ThreadLocalMap变量,这个变量存储着键值对形式的数据。

  • key为this,也就是当前ThreadLocal变量。

  • value为T,也就是要存储的值。

然后继续看看ThreadLocalMap哪来的,也就是getMap方法:

//ThreadLocal.java

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

//Thread.java

ThreadLocal.ThreadLocalMap threadLocals = null;

原来这个ThreadLocalMap变量是存储在线程类Thread中的。

所以ThreadLocal的基本机制就搞清楚了:

在每个线程中都有一个threadLocals变量,这个变量存储着ThreadLocal和对应的需要保存的对象。

这样带来的好处就是,在不同的线程,访问同一个ThreadLocal对象,但是能获取到的值却不一样。

挺神奇的是不是,其实就是其内部获取到的Map不同,Map和Thread绑定,所以虽然访问的是同一个ThreadLocal对象,但是访问的Map却不是同一个,所以取得值也不一样。

这样做有什么好处呢?为什么不直接用Map存储线程和对象呢?

打个比方:

  • ThreadLocal就是老师。

  • Thread就是同学。

  • Looper(需要的值)就是铅笔。

现在老师买了一批铅笔,然后想把这些铅笔发给同学们,怎么发呢?两种办法:

  • 1、老师把每个铅笔上写好每个同学的名字,放到一个大盒子里面去(map),用的时候就让同学们自己来找。

这种做法就是Map里面存储的是同学和铅笔,然后用的时候通过同学来从这个Map里找铅笔。

这种做法就有点像使用一个Map,存储所有的线程和对象,不好的地方就在于会很混乱,每个线程之间有了联系,也容易造成内存泄漏。

  • 2、老师把每个铅笔直接发给每个同学,放到同学的口袋里(map),用的时候每个同学从口袋里面拿出铅笔就可以了。

这种做法就是Map里面存储的是老师和铅笔,然后用的时候老师说一声,同学只需要从口袋里拿出来就行了。

很明显这种做法更科学,这也就是ThreadLocal的做法,因为铅笔本身就是同学自己在用,所以一开始就把铅笔交给同学自己保管是最好的,每个同学之间进行隔离。

13、还有哪些地方运用到了ThreadLocal机制?


比如:Choreographer。

public final class Choreographer {

// Thread local storage for the choreographer.

private static final ThreadLocal sThreadInstance =

new ThreadLocal() {

@Override

protected Choreographer initialValue() {

Looper looper = Looper.myLooper();

if (looper == null) {

throw new IllegalStateException(“The current thread must have a looper!”);

}

Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);

if (looper == Looper.getMainLooper()) {

mMainInstance = choreographer;

}

return choreographer;

}

};

private static volatile Choreographer mMainInstance;

Choreographer主要是主线程用的,用于配合 VSYNC 中断信号。

所以这里使用ThreadLocal更多的意义在于完成线程单例的功能。

14、可以多次创建Looper吗?


Looper的创建是通过Looper.prepare方法实现的,而在prepare方法中就判断了,当前线程是否存在Looper对象,如果有,就会直接抛出异常:

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

}

private Looper(boolean quitAllowed) {

mQueue = new MessageQueue(quitAllowed);

mThread = Thread.currentThread();

}

所以同一个线程,只能创建一个Looper,多次创建会报错。

15、Looper中的quitAllowed字段是啥?有什么用?


按照字面意思就是是否允许退出,我们看看他都在哪些地方用到了:

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

} else {

removeAllMessagesLocked();

}

}

}

哦,就是这个quit方法用到了,如果这个字段为false,代表不允许退出,就会报错。

但是这个quit方法又是干嘛的呢?从来没用过呢。 还有这个safe又是啥呢?

其实看名字就差不多能了解了,quit方法就是退出消息队列,终止消息循环。

  • 首先设置了mQuitting字段为true。

  • 然后判断是否安全退出,如果安全退出,就执行removeAllFutureMessagesLocked方法,它内部的逻辑是清空所有的延迟消息,之前没处理的非延迟消息还是需要取处理,然后设置非延迟消息的下一个节点为空(p.next=null)。

  • 如果不是安全退出,就执行removeAllMessagesLocked方法,直接清空所有的消息,然后设置消息队列指向空(mMessages = null)

然后看看当调用quit方法之后,消息的发送和处理:

//消息发送

boolean enqueueMessage(Message msg, long when) {

synchronized (this) {

if (mQuitting) {

IllegalStateException e = new IllegalStateException(

msg.target + " sending message to a Handler on a dead thread");

Log.w(TAG, e.getMessage(), e);

msg.recycle();

return false;

}

}

当调用了quit方法之后,mQuitting为true,消息就发不出去了,会报错。

再看看消息的处理,loop和next方法:

Message next() {

for (;😉 {

synchronized (this) {

if (mQuitting) {

dispose();

return null;

}

}

}

}

public static void loop() {

for (;😉 {

Message msg = queue.next();

if (msg == null) {

// No message indicates that the message queue is quitting.

return;

}

}

}

很明显,当mQuitting为true的时候,next方法返回null,那么loop方法中就会退出死循环。

那么这个quit方法一般是什么时候使用呢?

  • 主线程中,一般情况下肯定不能退出,因为退出后主线程就停止了。所以是当APP需要退出的时候,就会调用quit方法,涉及到的消息是EXIT_APPLICATION,大家可以搜索下。

  • 子线程中,如果消息都处理完了,就需要调用quit方法停止消息循环。

16、Looper.loop方法是死循环,为什么不会卡死(ANR)?


我大致总结下:

  • 1、主线程本身就是需要一只运行的,因为要处理各个View,界面变化。所以需要这个死循环来保证主线程一直执行下去,不会被退出。

  • 2、真正会卡死的操作是在某个消息处理的时候操作时间过长,导致掉帧、ANR,而不是loop方法本身。

  • 3、在主线程以外,会有其他的线程来处理接受其他进程的事件,比如Binder线程(ApplicationThread),会接受AMS发送来的事件

  • 4、在收到跨进程消息后,会交给主线程的Hanlder再进行消息分发。所以Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施,比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终执行到onCreate方法。

  • 5、当没有消息的时候,会阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生。所以死循环也不会特别消耗CPU资源。

17、Message是怎么找到它所属的Handler然后进行分发的?


在loop方法中,找到要处理的Message,然后调用了这么一句代码处理消息:

msg.target.dispatchMessage(msg);

所以是将消息交给了msg.target来处理,那么这个target是啥呢?

找找它的来头:

//Handler

private boolean enqueueMessage(MessageQueue queue,Message msg,long uptimeMillis) {

msg.target = this;

return queue.enqueueMessage(msg, uptimeMillis);

}

在使用Hanlder发送消息的时候,会设置msg.target = this,所以target就是当初把消息加到消息队列的那个Handler。

18、Handler 的 post(Runnable) 与 sendMessage 有什么区别


Hanlder中主要的发送消息可以分为两种:

  • post(Runnable)

  • sendMessage

public final boolean post(@NonNull Runnable r) {

return sendMessageDelayed(getPostMessage®, 0);

}

private static Message getPostMessage(Runnable r) {

Message m = Message.obtain();

m.callback = r;

return m;

}

通过post的源码可知,其实post和sendMessage的区别就在于:

post方法给Message设置了一个callback

那么这个callback有什么用呢?我们再转到消息处理的方法dispatchMessage中看看:

public void dispatchMessage(@NonNull Message msg) {

if (msg.callback != null) {

handleCallback(msg);

} else {

if (mCallback != null) {

if (mCallback.handleMessage(msg)) {

return;

}

}

handleMessage(msg);

}

}

private static void handleCallback(Message message) {

message.callback.run();

}

这段代码可以分为三部分看:

  • 1、如果msg.callback不为空,也就是通过post方法发送消息的时候,会把消息交给这个msg.callback进行处理,然后就没有后续了。

  • 2、如果msg.callback为空,也就是通过sendMessage发送消息的时候,会判断Handler当前的mCallback是否为空,如果不为空就交给Handler.Callback.handleMessage处理。

  • 3、如果mCallback.handleMessage返回true,则无后续了。

  • 4、如果mCallback.handleMessage返回false,则调用handler类重写的handleMessage方法。

所以post(Runnable) 与 sendMessage的区别就在于后续消息的处理方式,是交给msg.callback还是 Handler.Callback或者Handler.handleMessage

19、Handler.Callback.handleMessage 和 Handler.handleMessage 有什么不一样?为什么这么设计?


接着上面的代码说,这两个处理方法的区别在于Handler.Callback.handleMessage方法是否返回true:

  • 如果为true,则不再执行Handler.handleMessage

  • 如果为false,则两个方法都要执行。

那么什么时候有Callback,什么时候没有呢?这涉及到两种Hanlder的 创建方式:

val handler1= object : Handler(){

override fun handleMessage(msg: Message) {

super.handleMessage(msg)

}

}

val handler2 = Handler(object : Handler.Callback {

override fun handleMessage(msg: Message): Boolean {

return true

}

})

常用的方法就是第1种,派生一个Handler的子类并重写handleMessage方法。 而第2种就是系统给我们提供了一种不需要派生子类的使用方法,只需要传入一个Callback即可。

20、Handler、Looper、MessageQueue、线程是一一对应关系吗?


  • 一个线程只会有一个Looper对象,所以线程和Looper是一一对应的。

  • MessageQueue对象是在new Looper的时候创建的,所以Looper和MessageQueue是一一对应的。

  • Handler的作用只是将消息加到MessageQueue中,并后续取出消息后,根据消息的target字段分发给当初的那个handler,所以Handler对于Looper是可以多对一的,也就是多个Hanlder对象都可以用同一个线程、同一个Looper、同一个MessageQueue。

总结:Looper、MessageQueue、线程是一一对应关系,而他们与Handler是可以一对多的。

21、ActivityThread中做了哪些关于Handler的工作?(为什么主线程不需要单独创建Looper)


主要做了两件事:

  • 1、在main方法中,创建了主线程的LooperMessageQueue,并且调用loop方法开启了主线程的消息循环。

public static void main(String[] args) {

Looper.prepareMainLooper();

if (sMainThreadHandler == null) {

sMainThreadHandler = thread.getHandler();

}

Looper.loop();

throw new RuntimeException(“Main thread loop unexpectedly exited”);

}

  • 2、创建了一个Handler来进行四大组件的启动停止等事件处理

final H mH = new H();

class H extends Handler {

public static final int BIND_APPLICATION = 110;

public static final int EXIT_APPLICATION = 111;

public static final int RECEIVER = 113;

public static final int CREATE_SERVICE = 114;

public static final int STOP_SERVICE = 116;

public static final int BIND_SERVICE = 121;

22、IdleHandler是啥?有什么使用场景?


之前说过,当MessageQueue没有消息的时候,就会阻塞在next方法中,其实在阻塞之前,MessageQueue还会做一件事,就是检查是否存在IdleHandler,如果有,就会去执行它的queueIdle方法。

private IdleHandler[] mPendingIdleHandlers;

Message next() {

int pendingIdleHandlerCount = -1;

for (;😉 {

synchronized (this) {

//当消息执行完毕,就设置pendingIdleHandlerCount

if (pendingIdleHandlerCount < 0

&& (mMessages == null || now < mMessages.when)) {

pendingIdleHandlerCount = mIdleHandlers.size();

}

//初始化mPendingIdleHandlers

if (mPendingIdleHandlers == null) {

mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];

}

//mIdleHandlers转为数组

mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);

}

// 遍历数组,处理每个IdleHandler

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

}

//如果queueIdle方法返回false,则处理完就删除这个IdleHandler

if (!keep) {

synchronized (this) {

mIdleHandlers.remove(idler);

}

}

}

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

pendingIdleHandlerCount = 0;

}

}

当没有消息处理的时候,就会去处理这个mIdleHandlers集合里面的每个IdleHandler对象,并调用其queueIdle方法。 最后根据queueIdle返回值判断是否用完删除当前的IdleHandler

然后看看IdleHandler是怎么加进去的:

Looper.myQueue().addIdleHandler(new IdleHandler() {

@Override

public boolean queueIdle() {

//做事情

return false;

}

});

public void addIdleHandler(@NonNull IdleHandler handler) {

if (handler == null) {

throw new NullPointerException(“Can’t add a null IdleHandler”);

}

synchronized (this) {

mIdleHandlers.add(handler);

}

}

ok,综上所述,IdleHandler就是当消息队列里面没有当前要处理的消息了,需要堵塞之前,可以做一些空闲任务的处理。

常见的使用场景有:启动优化

我们一般会把一些事件(比如界面view的绘制、赋值)放到onCreate方法或者onResume方法中。 但是这两个方法其实都是在界面绘制之前调用的,也就是说一定程度上这两个方法的耗时会影响到启动时间。

所以我们可以把一些操作放到IdleHandler中,也就是界面绘制完成之后才去调用,这样就能减少启动时间了。

但是,这里需要注意下可能会有坑。

如果使用不当,IdleHandler会一直不执行,比如在View的onDraw方法里面无限制的直接或者间接调用View的invalidate方法

其原因就在于onDraw方法中执行invalidate,会添加一个同步屏障消息,在等到异步消息之前,会阻塞在next方法,而等到FrameDisplayEventReceiver异步任务之后又会执行onDraw方法,从而无限循环。

23、HandlerThread是啥?有什么使用场景?

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

尾声

如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

架构篇

《Jetpack全家桶打造全新Google标准架构模式》

减轻大家的负担。**
[外链图片转存中…(img-6ipdrCxt-1711804401016)]
[外链图片转存中…(img-7stbj8hs-1711804401017)]
[外链图片转存中…(img-KbkehQdW-1711804401018)]
[外链图片转存中…(img-A7X5DZax-1711804401018)]
[外链图片转存中…(img-F83gKZr4-1711804401018)]
[外链图片转存中…(img-DddDUpxR-1711804401019)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-iMHrapUj-1711804401019)]

尾声

如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。[外链图片转存中…(img-1zsm12jF-1711804401019)]

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-H73UnHof-1711804401019)]

架构篇

《Jetpack全家桶打造全新Google标准架构模式》
[外链图片转存中…(img-y0dFvn9B-1711804401020)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值