android 面试题handle系列

687 阅读13分钟

Handler

handle与loop关联

new handle的时候,handle就拿到了looper和messageQueue对象,那么他两个是在activitythread的mian方法里面创建的

handle流程和原理

sengMessage调用handle的enqueueMessage,里面调用了massageQueue的enqueueMessage进行入列 出列是looper里面有个loop函数,loop函数里面做了一个for的死循环,里面调用了messageQueue的next方法把消息取出来,这个过程是出列,然后紧接着在looper.loop的next方法下面执行msg.tagret.dispathMassage(此方法为handle的方法)调用到了handleMassage进行消息处理 一个线程有多个handle,一个线程只有一个looper,为什么只有一个looper?或者如何保证只有一个looper,looper的prepare方法里面,调用了threadlocal的set()方法,创建了一个looper对象传了进去,然后通过threadlocalmap吧当前的threadlocal和looper对象进行绑定,把looper绑定到当前thread上,回到prepare方法里,在调用set方法之前,调用了threadlocal的get方法进行了判断,在threadlocalmap里面查询当前threadlocal是否绑定过loop,如果绑定过,那么抛出异常,禁止创建looper,从而保证了当前threadlocal只能有一个looper,那么如何保证thread和threadlocal是一对一关系?thread在创建时,把thread设置为静态常量,保证了thread只存在一份, static final ThreadLocal sThreadLocal = new ThreadLocal;

1.handle实现原理 主要有四个对象,handle ,message,messagequeue,loop 在主线程入口类activitythread.java的main方法中,通过调用looper.prepareMainLooper()创建了looper和关联的messageQueue,new handle的时候在构造函数里面拿到了对应线程的looper和messagequeue对象,然后通过looper.loop方法不断的从messagequeue里面取出数据,这里面是个死循环,把取出来的message发送给对应的handle的dispatchmessage(msg.target.dispatchmessage)方法,在发送给handlemessage处理

2.1子线程中能不能直接new一个Handler 可以在子线程直接new一个Handler,不过需要在子线程里先调用Looper.prepare(),new一个Handler后,还需要调用Looper.loop()方法。

2.2 为什么主线程可以?主线程的Looper第一次调用loop方法,什么时候,哪个类 因为主线程入口类activityThread的main函数里面调用了looper.preparemainlooper,这个方法创建了looper和messagequeue,并且调用了looper.loop方法

3.一个线程可以有几个Handler,几个Looper,几个MessageQueue对象 一个线程可以有多个handle,只有一looper和一个messagequeue

4.Message对象创建的方式有哪些 & 区别? 在创建Message对象时,有三种创建方法,分别为: 1.Message message = new message(); 2.Message message = Message.obtain(); 3.Message message = handler.obtainMessage();

第三种内部其实也是实现了message.obtain方法,所以第二种和第三种作用相同,那第一种是每次都创建一个message对象,比较占用内存,而通过message.obtain方法是从message的缓存池里面获取一个新的message,节省了去申请对象空间的时间,也减小了内存使用和gc频繁回收的压力,提高效率(message是单链表结构,里面是栈结构的缓冲池)

5.Message.obtain()怎么维护消息池的 使用了享元设计模式,当前message执行完后,把message置为空,然后重新给message进行复制。 通过链表的形式,进行了复用和回收

6.Handler 有哪些发送消息的方法 handle.sendmessage(重写handlemessage方法) handle.post(重写runable.run方法)

6.1.区别

sendmessage更新ui线程的操作是在handlemessage里面执行,而post这个方法是把任务run转成一个message放进了handler所在的线程中的messageQueue消息队列中,可以在子线程中重写runnable.run方法,在此方法里面更新ui,然后这个任务会被添加到主线程的handle的messagequeue里面,在post方法中message是通过getPostMessage(Runnable r)这个方法获取的message进行处理

7.handler postDealy后消息队列有什么变化,假设先 postDelay 10s, 再postDelay 1s, 怎么处理这2条消息

如果队列中只有这个消息,那么消息不会被发送,而是计算到时唤醒的时间,先将Looper阻塞,到时间就唤醒它。但如果此时要加入新消息,该消息队列的对头跟delay时间相比更长,则插入到头部,按照触发时间进行排序,队头的时间最小、队尾的时间最大。

8.Handler 引起的内存泄露原因以及最佳解决方案 handle里面允许延迟发送消息,如果在延迟发送期间,关闭了此activity,但是message还持用handle的对象,在java特性中,内部类持有外部类,所以activity会被handle持有,但是activity已经销毁,所以会导致activity泄漏。 解决方案:将 Handler 定义成静态的内部类,在内部持有Activity的弱引用,并在Acitivity的onDestroy()中调用handler.removeCallbacksAndMessages(null)及时移除所有消息。

9.MessageQueue是什么数据结构 单向链表,先进先出

10.ThreadLocal在Handler机制中的作用

threadlocal内部是一个map实现,以当前线程threadlocal为键,以looper为值进行绑定,保证一个线程对应一个looper

11.HandlerThread是什么 & 好处 &原理 & 使用场景

  1. HandlerThread继承于Thread,本质上也是一个线程。
  2. HandlerThread的run方法为本线程创建提供了Looper和MessageQueue对象,并开启了Looper轮询消息。
  3. 最后需要注意的是在我们不需要这个looper线程的时候需要手动停止掉,即调用quit()或者quitSafely()。 使用场景: 最后补充一个在实际开发过程中使用到HandlerThread的场景:存在多个耗时的任务需要放到开启子线程依次去处理(串行处理任务),首先,HandlerThread是一个子线程,适合处理耗时的任务,其次,Handler分发消息是通过MessageQueue顶部的Message不断的通过Message的next依次取出Message,符合任务的按顺序串行处理的要求,所以使用HandlerThread就能完美的解决此需求。

12.IdleHandler及其使用场景

IdleHandler 被定义在 MessageQueue 中,它是一个接口。当一个线程要阻塞等待更多消息时会被回调。就是当UI线程执行完毕后,会调用该方法。当queueIdle()方法返回true,idleHandler一直处于激活状态,如果返回false,就会被移除。IdleHandler 主要是在 MessageQueue 出现空闲的时候被执行,那么何时出现空闲?-

  • MessageQueue 为空,没有消息;

  • MessageQueue 中最近需要处理的消息,是一个延迟消息(when>currentTime),需要滞后执行; 场景:

  1. 在应用启动时我们可能希望把一些优先级没那么高的操作延迟一点处理,一般会使用 Handler.postDelayed(Runnable r, long delayMillis)来实现,但是又不知道该延迟多少时间比较合适,因为手机性能不同,有的性能较差可能需要延迟较多,有的性能较好可以允许较少的延迟时间。所以在做项目性能优化的时候可以使用 IdleHandler,它在主线程空闲时执行任务,而不影响其他任务的执行。
  2. 想要在一个 View 绘制完成之后添加其他依赖于这个 View 的 View,当然这个用View.post()也能实现,区别就是前者会在消息队列空闲时执行
  3. 发送一个返回 true 的 IdleHandler,在里面让某个 View 不停闪烁,这样当用户发呆时就可以诱导用户点击这个View,这也是种很酷的操作

13.子线程能不能更新UI

  • 子线程可以在ViewRootImpl或者super.onresume()方法之前还没有被创建之前更新UI; 在oncreate方法里新建子线程,并更新ui(textview.settext()),发现不一定会崩溃,但是延迟几秒后在更新UI就会报错,原因是在oncreate方法中,viewrootimpi还没有创建,他是在onresume方法之后才创建的,所以不会执行viewrootimpi中的checkthread(检测是否在主线程)方法,但是延迟几秒后,viewrootimpi已经创建完成,所以程序崩溃
  • 访问UI是没有加对象锁的,在子线程环境下更新UI,会造成不可预期的风险;
  • 开发者更新UI一定要在主线程进行操作;

14.为什么Android系统不建议子线程访问UI

1.谷歌提出:“一定要在主线程更新UI”,实际是为了提高界面的效率和安全性,带来更好的流畅性;反推一下,假如允许多线程更新UI,但是访问UI是没有加锁的,一旦多线程抢占了资源,那么界面将会乱套更新了,体验效果就不言而喻了;所以在Android中规定必须在主线程更新UI。 因为UI控件不是线程安全的。那为啥不加锁呢?

2.一是加锁会让UI访问变得复杂;二是加锁会降低UI访问效率,会阻塞一些线程访问UI。所以干脆使用单线程模型处理UI操作,使用时用Handler切换即可。

15.Android中为什么主线程不会因为Looper.loop()里的死循环卡死

主线程确实是阻塞的,不阻塞那APP怎么能一直运行,所以说主线程阻塞是一个伪命题,只不过是没有弄明白既然阻塞了,为什么还能调用各种声明周期而已.
调用生命周期是因为有Looper,有MessageQueue,还有沟通的桥梁Handler,通过IPC机制调用Handler发送各种消息,保存到MessageQueue中,然后在主线程中的Looper提取了消息,并在主线程中调用Handler的方法去处理消息.最终完成各种声明周期.

真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。 主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/e poll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。 Activity的生命周期是怎么实现在死循环体外能够执行起来的?

ActivityThread的内部类H继承于Handler,通过handler消息机制,简单说Handler机制用于同一个进程的线程间通信。

Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:
在H.handleMessage(msg)方法中,根据接收到不同的msg,执行相应的生命周期。

16.- MessageQueue.next 在没有消息的时候会阻塞,如何恢复? 当其他线程调用 MessageQueue#enqueueMessage 时会唤醒 MessageQueue,这个方法会被 Handler#sendMessageHandler#post 等一系列发送消息的方法调用。

17.Handler消息机制中,一个looper是如何区分多个Handler的

1.msg的target持有一个发送此消息的Handler引用。 2.Looper.loop()会阻塞于MessageQueue.next()
3.取出msg后,msg.target成员变量就是该msg对应的Handler
4.调用msg.target的disptachMessage()进行消息分发。

18. 当Activity有多个Handler的时候,怎么样区分当前消息由哪个Handler处理

在looper.loop中,looper吧message直接交给了target即发送这个消息的handler处理

19.Looper.quit/quitSafely的区别 二者都调用了MessageQueue中的quit方法,quit传参是true,quitsafely传参是false, 当我们调用Looper的quit方法时,实际上执行了MessageQueue中的removeAllMessagesLocked方法,该方法的作用是把MessageQueue消息池中所有的消息全部清空,无论是延迟消息(延迟消息是指通过sendMessageDelayed或通过postDelayed等方法发送的需要延迟执行的消息)还是非延迟消息。当我们调用Looper的quitSafely方法时,实际上执行了MessageQueue中的removeAllFutureMessagesLocked方法,通过名字就可以看出,该方法只会清空MessageQueue消息池中所有的延迟消息,并将消息池中所有的非延迟消息派发出去让Handler去处理,quitSafely相比于quit方法安全之处在于清空消息之前会派发所有的非延迟消息。

无论是调用了quit方法还是quitSafely方法只会,Looper就不再接收新的消息。即在调用了Looper的quit或quitSafely方法之后,消息循环就终结了,这时候再通过Handler调用sendMessage或post等方法发送消息时均返回false,表示消息没有成功放入消息队列MessageQueue中,因为消息队列已经退出了。

20.通过Handler如何实现线程的切换 实际线程间切换,就是通过线程间共享变量实现的。

在A线程new handler(),在b线程调用这个handler发送消息,这个message发送到了,A线程中的 messageQueue里面,又回到了a线程中执行。

handler 持有Looper实例,looper有持有messageQueue实例,   handler 把message发送到了 当前持有的messageQueue实例中。这个messageQueue 在new handler之前,已经通过当前线程的Looper.prepare创建了。

**21.Handler 如何与 Looper 关联的

通过threadLocal进行关联的

22.looper与thread如何关联的

通过threadLocal进行关联的

23.MessageQueue的enqueueMessage()方法如何进行线程同步的

  如果此消息是延时的消息,则将其添加到队列中,原理就是链表的添加新元素,按照when,也就是延迟的时间来插入的,延迟的时间越长,越靠后,这样就得到一条有序的延时消息链表,取出消息的时候,延迟时间越小的,就被先获取了。
  

24.MessageQueue的next()方法内部原理 for循环取消息,在消息队列为空时,调用nativepollonce方法进行等待 25.唤醒机制 唤醒机制是,在调用enqueuemessage把message放入消息队列是,调用了nitivewake方法,这个方法是通知底层把线程进行唤醒,然后把消息取出来判断是否需要立刻执行,因为可能是延迟消息,在调用next方法是,如果当前队列中的message为空,那么会调用nativepollonce进行等待。

26.AMS与handle关系

loop不能停,activitythread很多操作都是使用handle实现的,activity生命周期,点击事件,启动程序等等

27.handle如何处理延迟消息 利用当前系统时间加上延迟时间来作为优先级进入入队处理的,比如延迟了2秒,系统时间加上2秒正好在第三个消息到第四个消息之间,那么就把这个消息插入到第三到第四之间来执行