Handler面试必问10点

790 阅读11分钟

谈谈消息机制Handler作用?有哪些要素?流程是怎样的?简单说一下你的看法!

handler是把一个任务发送的某一个线程去执行的机制 构成这套机制的有Handler、Looper、messageQueue 、message,Handler负责消息的发送的执行,messageQueue负责消息的存和取,Looper是一个轮询, 不断从队列中取出消息交给对应的handler执行 在某个线程中通过调用Looper.prepare()方法创建绑定该线程的Looper和MessageQueue,调用Looper.loop()方法开启轮询,通过调用MessageQueue.next()查看是否有可执行的消息,handler的sendmessage其实调用了MessageQueue的enqueue()方法把消息插入自身的消息链表中 , 当取出了可用的消息后通过message的target交给对应的handler并执行handlemessage()方法

为什么一个线程只有一个Looper、只有一个MessageQueue,可以有多个Handler?

Looper的构造方法是private的,初始化一个Looper只能使用它的静态方法prepare()或者prepareMain()方法,方法中会new创建一个Looper并通过ThreaLocal把Looper保存到当前线程的ThreadLoclMap变量中,Threadlocal的特性决定了一个线程只有一个数据副本,并且在Looper.prepare()中如果发现该线程中已经存在了looper就回抛出异常 MessageQueue是在Looper的构造方法中创建的,Looper在一个线程中只能创建一次,自然MessageQueue也只能被创建一次 handle是负责发送和执行消息的,不同的handler处理消息的逻辑也会不同,所以handler可以是多个

如何在子线程中使用handler?

因为子线程默认是没有Looper,所以要在子线程使用handler,需要先调用Looper.prepare()创建Looper和messageQueue,然后调用Looper.loop()开启轮询,最后创建handler的时候需要把looper作为构造参数传递进去,绑定后才可以使用 因为线程就是一段可执行的代码,执行完成之后线程会被销毁,但是调用Looper.loop()会开启轮询,导致线程不能销毁,所以,不再需要的时候要调用Looper.quie()退出轮询,结束线程

MessageQueue的作用

MessageQueue作用是管理消息的存(enqueueMessage)和取(next),并且作为java层和native层的桥接,通过aidl跟native层的MessageQueue链接起来,实现native和java层的相互调用。

next方法的逻辑

for(:;:)死循环,调用nativePollOnce(mPtr,nextPollTimeoutMillis),这个方法会主线程阻塞等待nextPollTimeoutMillis时长,或者有消息到来,通过nativeWake()主动唤醒。第一次调用next()时,nextPollTimeoutMillis=0,即不阻塞。 随后取出链表的头元素,因为调用enqueueMessage()是按照执行时间的先后顺序把Message插入到链表的,所有头元素就是即将执行的那个消息,如果Message.target=null,表示开启了同步屏障,就跳过同步消息,找出链表中下一个异步消息

如果可执行消息为空,或者执行时间还没到(设置nextPollTimeoutMillis=when-now),检查是否给MessageQueue添加了IdleHandler,如果有值,就遍历所有的IdleHandler并执行它queueIdle()方法,然后重置nextPollTimeoutMillis=0,因为执行IdleHandler后已经到达了消息的执行时间。 如果没有添加IdleHandler,就直接调用continue,返回循环,执行nativePollOnce(mPtr,nextPollTimeoutMillis),阻塞主线程。

nativePollOnce方法是如何阻塞主线程的

了解主线程阻塞原理前,需要理解Liunx的PipeEpoll机制 Epoll是Liunx下的一种IO多路复用机制,那什么是多路复用呢?

首先要理解的是,系统给你提供一种功能,当某个socket可读或者可写的时候,系统给你一个通知,这时候配合非阻塞socket,只有当系统通知我那个描述符可读了,我才去进行read操作,这样就能保证每次执行read的时候能够读到有效数据,而不是返回-1等无用功。操作系统的这个功能通过select/poll/epoll等函数来实现,它可以同时监听多个描述符的读写状态,这样多个描述符的IO操作就可以在一个线程中并发交替完成,这里的复用指的是复用同一个线程。 Epoll是其中一种实现,它可以监听多个fd,有以下三个重要的函数 epoll_create() 创建一个epoll实例,并返回相应的fd epoll_ctl() 注册需要监听的描述符和监听事件 epoll_wait() 等待描述符有可用的事件,如果当前没有可用的事件,会阻塞调用的线程,直到事件可用或者超时 匿名管道(Pipe)是Liunx线程或者进程间通信的一种方式,通过pipe(fd[])函数创建管道的文件描述符,分别代表管道的读端和管道的写端。调用read从读端读取数据,调用write从写端写入数据 通常Epoll和Pipe配合使用时,把读端或者写端的fd和事件注册到epoll中,调用wait()等待事件可用,当事件可用后,wait()函数返回,然后进行相应的操作。比如监听读事件,当往管道的写端写入数据后,读事件可用,wait()函数返回,调用线程从阻塞态变为运行态,就可以调用read()函数从管道中读出数据。


创建Looper的同时会创建MessageQueue,MessageQueue的构造方法中在会nativeInit(),这是一个native方法,该方法会创建native层的MessageQueue并赋值给java层的mPtr,同时创建一个native层的Looper,Looper的构造方法中做了几件很重要的事情

1.创建一个用于实现唤醒的读写管道fd(mWakeReadFd/mWakeWriteFd) 2.调用epoll_create()创建一个epoll监听池 3.调用epoll_ctl()注册管道的读端fd和读事件 通过以上操作,epoll已经准备好监听管道读端的读数据事件了 调用next()方法取消息的时候会调用nativePollOnce(),=最终调用到native层Looper的pollOnce()方法,该方法中通过调用epoll_wait()阻塞主线程,等待读管道的可读通知。知道写管道有数据写入或者等待超时后,主线程被唤醒 那么什么时候会往写管道写入数据,唤醒主线程呢?答案就是MessageQueue的nativeWake()方法,这个方法最终调用nativeLooper的wake()方法,通过调用write()方法往管道中写入一个字符“1”,然后epoll的wait()返回,唤醒主线程 总结一下,消息机制的主线程阻塞是通过Liunx系统的Epoll机制实现的,通过epoll监听一个读写管道,当消息队列中没有消息的时候,会调用epoll_wait()阻塞主线程,当有消息来临,通过向管道的写端写入一个字符1,系统通知有可读数据,epoll_wait()函数返回,唤醒主线程。

同步屏障是如何实现的

通过调用MessageQueue.postSyncBarrier()就可以开启消息队列同步屏障,开启后,next()取出消息的时候会跳过同步消息,异步消息不受影响

开始读这段源码的时候很疑惑,为什么msg.target==null就表示开启的同步屏障呢? 其实看一看postSyncBarrier()的实现就一目了然了 逻辑很简单,就是创建了一个target为空的Message,然后把消息插入到Message链表的头部。而开发者使用Handler的方法发送一个消息的时候,都会把Message的target设置为当前handler,所以如果Message链表中有一个message.target==null就认为开启了同步屏障

说一下Handler内存泄漏有哪些?造成造成内存泄漏原因是什么?如何解决handler造成的内存泄漏?

通常是handler作为非静态内部类或者匿名类持有的外部类(一般是activity)的引用,造成外部类被泄漏 原因是handler发送的message通过target持有了handler的引用,Handler又持有外部类的引用,如果消息在消息队列中没有没有被执行,而handler持有的外部类又被销毁的时候,就会导致这个外部类被泄漏 解决办法就是不让handler持有外部类的引用,可以使用外部单独的Handler或者静态内部类Handler,当要使用外部类的时候使用弱引用包装,最后是在外部类声明周期结束的时候调用messagequeue的removemessageandcallback()方法清理掉没有执行的message

主线程如何自动绑定Looper?主线程中的Looper死循环和binder线程中的死循环有哪些区别?

程序进程启动的时候,会在进程中加载ActivityThread这个类并执行它的main()方法,这个方法中会通过Looper.prepare()和Looper.loop()创建主线程looper并开启轮询 //TODO binder]

为什么系统不建议在子线程访问UI,不对UI控件的访问加上锁机制的原因?

因为ui控件不是线程安全的,如果在多个线程并发访问会导致ui处于一个不可控的状态 对于加锁机制

  • 降低ui访问的效率
  • 首先加上锁机制会让UI访问的逻辑变得复杂 所以最好的方法还是使用单线程模型

Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR或者卡顿?

卡顿的本质是在帧刷新时间也就是60ms内,没有绘制工作,等下一个syn信号到来的时候,用户看到的还是上一帧的内容,给用户的感觉就是界面卡主了 ANR的原因是事件处理的时间太长,导致不能及时进行帧刷新 所以 线程的本质是一段可执行的代码片段,代码执行结束,线程声明周期就终止,线程退出,但是对于主线程肯定是不能执行完代码就退出了,最简单的办法就是阻塞,没有消息的时候休眠。 android系统是基于事件驱动的,常见的点击触摸声明周期控制都是依靠handler机制来完成的,如果主线程处于阻塞状态,说明没有消息需要处理了 导致anr的原因有2个

  • 事件不能被及时处理
  • 事件处理的时间过长影响界面刷新

Handler.sendMessageDelayed()怎么实现延迟的?

MessageQueue.enqueue()会根据message的when属性即是消息的执行时间按照有小到大的顺序插入消息队列的链表中,在next()取出消息的方法中会根据当前当前时间和消息的执行时间做对比,取出当前可以执行的消息

用什么方式存储Message

用链表来存储的,不管以哪种方式发消息,最终都会调用messageQueue.enqueue方法,会遍历链表,根据消息的执行时间(SystemClock.uptimeMillis()+delayTime)把message插入到合适的位置,相当于在一个有序链表中插入一个元素😺

Message可以如何创建?哪种效果更好,为什么?

可以直接new一个,也可以调用message.obtain()方法,推荐使用第二种,因为这是从消息池中返回一个实例,可以避免创建新的对象

MessageQueue作用是干什么的?MessageQueue主要工作原理是怎样的?

MessageQueue负责消息的存和取,主要是2个方法enqueue()和next() enqueue()方法会把消息按照执行时间顺序插入到内部的链表中 next()轮询遍历整个链表取出当前可执行的消息,交给message的target也就是发送这个消息的handler去执行

ThreadLocal有什么作用?

每一个线程对象里面都有一个ThreadLocalMap对象,key就是线程自己,value是object,ThreadLocalMap跟hashMap类似,也是用散列表实现的,但是用在再寻址发解决冲突,因为数据量比较小

ThreadLocal就是一个工具类,get和set方法,都是先取出调用方法的线程的ThreadLocalMap,然后进行调用ThreadLocalMap的get和put方法,而ThreadLocalMap的key始终是线程本身

这样就保证了一个数据在同一个线程中,只会存在一份,也就是通过这种方式保证一个线程中只有一个Looper

IdHandler

通过messageQueue.addIdleHandler()方法添加,它的queueIdle()方法会在消息队列中没有消息,准备进入线程阻塞之前被调用,返回false执行一次后移除自己,返回true可以在每次满足上述条件时被重复执行

在首帧绘制的消息被执行后,添加的idleHandler会被执行,利用这个特性,可以用来统计从application界?面第一帧可见的

子线程一定不能刷新UI?

线程检查工作是在ViewRootImp的requestLayout里面checkThread()进行的,就是检查当前线是否跟创建ViewRootImp的线程一致,而ViewRootImp又是在调用windowManager.addView时创建的,所以,下面2中情况下,子线程也是可以更新UI的

  • 让ViewRootImp在子线程中创建,然后在相同的线程中调用界面刷新方法,虽然我不知道为啥要这么做??
  • 界面刷新工作不设计到重新测量或者布局,也就是说不会调用到requestLayout,就不会触发线程检查,比如setTextColor