Framework源码面试六部曲:3.Handler面试集合
Posted 初一十五啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Framework源码面试六部曲:3.Handler面试集合相关的知识,希望对你有一定的参考价值。
前言
今天在电脑上翻出了很久之前整理笔记Framework
源码面试,Flutter
,以及一部分面试专题。拿出来温习一下。
今天先讲Framework
源码篇:
1.
Framework
源码面试:1.Activity
启动流程
2.Framework
源码面试:2.Binder
面试
3.Framework
源码面试:3.Handler
面试
4.Framework
源码面试:4.事件分发机制
5.Framework
源码面试:5.onMeasure
测量原理
6.Framework
源码面试:6.android
屏幕刷新机制
1.Handler
怎么在主线程和子线程进行数据交互的原理?
主线程和子线程通过handler
交互,交互的载体是通过Message
这个对象,实际上我们在子线程发送的所有消息,都会加入到主线程的消息队列中,然后主线程分发这些消息,这个就很容易做到俩个线程信息的交互。
看到这里,你可能有疑问了,我从子线程发送的消息,怎么就加到了主线程的消息队列里呢???
大家可以看看你自己的代码,你的handler
对象是不是在主线程初始的?子线程发送消息,是不是通过这个handler
发送的?
这就很简单了,handler
只需要把发送的消息,加到自身持有的Looper
对象的MessageQueue
里面(mLooper
变量)就ok了
所以,你在哪个线程里面初始化Handler
对象,在不同的线程中,使用这个对象发送消息;都会在你初始化Handler
对象的线程里分发消息。
2.Handler
中主线程的消息队列是否有数量上限?为什么?
这问题整的有点鸡贼,可能会让你想到,是否有上限这方面?而不是直接想到到上限数量是多少?
解答:Handler
主线程的消息队列肯定是有上限的,每个线程只能实例化一个Looper
实例(上面讲了,Looper.prepare
只能使用一次),不然会抛异常,消息队列是存在Looper()
中的,且仅维护一个消息队列
重点:每个线程只能实例化一次Looper()
实例、消息队列存在Looper
中
拓展:MessageQueue
类,其实都是在维护mMessage
,只需要维护这个头结点,就能维护整个消息链表
3.Handler
中有Loop
死循环,为什么没有卡死?为什么没有发生ANR
?
先说下ANR
:5秒内无法响应屏幕触摸事件或键盘输入事件;广播的onReceive()
函数时10秒没有处理完成;前台服务20秒内,后台服务在200秒内没有执行完毕;ContentProvider
的publish
在10s内没进行完。所以大致上Loop
死循环和ANR
联系不大,问了个正确的废话,所以触发事件后,耗时操作还是要放在子线程处理,handler
将数据通讯到主线程,进行相关处理。
线程实质上是一段可运行的代码片,运行完之后,线程就会自动销毁。当然,我们肯定不希望主线程被over,所以整一个死循环让线程保活。
为什么没被卡死:在事件分发里面分析了,在获取消息的next()
方法中,如果没有消息,会触发nativePollOnce
方法进入线程休眠状态,释放CPU资源,MessageQueue
中有个原生方法nativeWake
方法,可以解除nativePollOnce
的休眠状态,ok,咱们在这俩个方法的基础上来给出答案。
-
当消息队列中消息为空时,触发
MessageQueue
中的nativePollOnce
方法,线程休眠,释放CPU资源 -
消息插入消息队列,会触发
nativeWake
唤醒方法,解除主线程的休眠状态- 当插入消息到消息队列中,为消息队列头结点的时候,会触发唤醒方法
- 当插入消息到消息队列中,在头结点之后,链中位置的时候,不会触发唤醒方法
综上:消息队列为空,会阻塞主线程,释放资源;消息队列为空,插入消息时候,会触发唤醒机制
- 这套逻辑能保证主线程最大程度利用CPU资源,且能及时休眠自身,不会造成资源浪费
本质上,主线程的运行,整体上都是以事件(Message
)为驱动的。
4.为什么不建议在子线程中更新UI?
多线程操作,在UI的绘制方法表示这不安全,不稳定。
假设一种场景:我会需要对一个圆进行改变,A线程将圆增大俩倍,B改变圆颜色。A线程增加了圆三分之一体积的时候,B线程此时,读取了圆此时的数据,进行改变颜色的操作;最后的结果,可能会导致,大小颜色都不对。。。
5.可以让自己发送的消息优先被执行吗?原理是什么?
这个问题,我感觉只能说:在有同步屏障的情况下是可以的。
同步屏障作用:在含有同步屏障的消息队列,会及时的屏蔽消息队列中所有同步消息的分发,放行异步消息的分发。
在含有同步屏障的情况,我可以将自己的消息设置为异步消息,可以起到优先被执行的效果。
6.子线程和子线程使用Handler
进行通信,存在什么弊端?
子线程和子线程使用Handler
通信,某个接受消息的子线程肯定使用实例化handler
,肯定会有Looper
操作,Looper.loop()
内部含有一个死循环,会导致线程的代码块无法被执行完,该线程始终存在。
如果在完成通信操作,我们一般可以使用: mHandler.getLooper().quit()
来结束分发操作
说明下:quit()
方法进行几项操作
- 清空消息队列(未分发的消息,不再分发了)
- 调用了原生的销毁方法
nativeDestroy
(猜测下:可能是一些资源的释放和销毁) - 拒绝新消息进入消息队列
- 它可以起到结束
loop()
死循环分发消息的操作
拓展:quitSafely()
可以确保所有未完成的事情完成后,再结束消息分发。
7.Handler
中的阻塞唤醒机制?
这个阻塞唤醒机制是基于 Linux 的 I/O 多路复用机制 epoll
实现的,它可以同时监控多个文件描述符,当某个文件描述符就绪时,会通知对应程序进行读/写操作.
MessageQueue
创建时会调用到 nativeInit
,创建新的 epoll
描述符,然后进行一些初始化并监听相应的文件描述符,调用了epoll_wait
方法后,会进入阻塞状态;nativeWake
触发对操作符的 write
方法,监听该操作符被回调,结束阻塞状态。
8.什么是IdleHandler
?什么条件下触发IdleHandler
?
IdleHandler
的本质就是接口,为了在消息分发空闲的时候,能处理一些事情而设计出来的
具体条件:消息队列为空的时候、发送延时消息的时候
9.消息处理完后,是直接销毁吗?还是被回收?如果被回收,有最大容量吗?
Handler
存在消息池的概念,处理完的消息会被重置数据,采用头插法进入消息池,取的话也直接取头结点,这样会节省时间
消息池最大容量为50,达到最大容量后,不再接受消息进入
10.不当的使用Handler
,为什么会出现内存泄漏?怎么解决?
先说明下,Looper
对象在主线程中,整个生命周期都是存在的,MessageQueue
是在Looper
对象中,也就是消息队列也是存在在整个主线程中;我们知道Message
是需要持有Handler
实例的,Handler
又是和Activity
存在强引用关系
存在某种场景:我们关闭当前Activity
的时候,当前Activity
发送的Message
,在消息队列还未被处理,Looper
间接持有当前activity
引用,因为俩者直接是强引用,无法断开,会导致当前Activity
无法被回收
思路:断开俩者之间的引用、处理完分发的消息,消息被处理后,之间的引用会被重置断开
解决:使用静态内部类弱引Activity
、清空消息队列
Handler的作用:
当我们需要在子线程处理耗时的操作(例如访问网络,数据库的操作),而当耗时的操作完成后,需要更新UI,这就需要使用Handler来处理,因为子线程不能做更新UI的操作。Handler能帮我们很容易的把任务(在子线程处理)切换回它所在的线程。简单理解,Handler就是解决线程和线程之间的通信的。
Handler连环之说说Handler的作用,以及每个类让他们的角色
使用的handler的两种形式
1.在主线程使用handler;
2.在子线程使用handler。
Handler的消息处理主要有五个部分组成
Message
Handler
Message Queue
Looper
ThreadLocal
首先简要的了解这些对象的概念
- Message:Message是在线程之间传递的消息,它可以在内部携带少量的数据,用于线程之间交换数据。Message有四个常用的字段,what字段,arg1字段,arg2字段,obj字段。what,arg1,arg2可以携带整型数据,obj可以携带object对象。
- Handler:它主要用于发送和处理消息的发送消息一般使用
sendMessage()
方法,还有其他的一系列sendXXX
的方法,但最终都是调用了sendMessageAtTime
方法,除了sendMessageAtFrontOfQueue()
这个方法 而发出的消息经过一系列的辗转处理后,最终会传递到Handler的handleMessage
方法中。 - Message Queue:
MessageQueue
是消息队列的意思,它主要用于存放所有通过Handler发送的消息,这部分的消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue
对象。 - Looper:每个线程通过Handler发送的消息都保存在,
MessageQueue
中,Looper
通过调用loop()
的方法,就会进入到一个无限循环当中,然后每当发现Message Queue
中存在一条消息,就会将它取出,并传递到Handler的handleMessage()
方法中。每个线程中只会有一个Looper
对象。
ThreadLocal:MessageQueue
对象,和Looper
对象在每个线程中都只会有一个对象,怎么能保证它只有一个对象,就通过ThreadLocal
来保存。Thread Local
是一个线程内部的数据存储类,通过它可以在指定线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储到数据,对于其他线程来说则无法获取到数据。
Handler连环泡之 说说 Looper 死循环为什么不会导致应用卡死?
线程默认没有
Looper
的,如果需要使用Handler就必须为线程创建Looper
。我们经常提到的主线程,也叫UI线程,它就是ActivityThread
,ActivityThread
被创建时就会初始化Looper
,这也是在主线程中默认可以使用Handler的原因。
首先我们看一段代码
new Thread(new Runnable()
@Override
public void run()
Log.e("qdx", "step 0 ");
Looper.prepare();
Toast.makeText(MainActivity.this, "run on Thread", Toast.LENGTH_SHORT).show();
Log.e("qdx", "step 1 ");
Looper.loop();
Log.e("qdx", "step 2 ");
).start();
我们知道Looper.loop()
;里面维护了一个死循环方法,所以按照理论,上述代码执行的应该是 step 0 –>step 1 也就是说循环在Looper.prepare()
;与Looper.loop()
;之间。
在子线程中,如果手动为其创建了
Looper
,那么在所有的事情完成以后应该调用quit
方法来终止消息循环,否则这个子线程就会一直处于等待(阻塞)状态,而如果退出Looper
以后,这个线程就会立刻(执行所有方法并)终止,因此建议不需要的时候终止Looper
。
执行结果也正如我们所说,这时候如果了解了ActivityThread
,并且在main
方法中我们会看到主线程也是通过Looper
方式来维持一个消息循环
public static void main(String[] args)
Looper.prepareMainLooper();//创建Looper和MessageQueue对象,用于处理主线程的消息
ActivityThread thread = new ActivityThread();
thread.attach(false);//建立Binder通道 (创建新线程)
if (sMainThreadHandler == null)
sMainThreadHandler = thread.getHandler();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
//如果能执行下面方法,说明应用崩溃或者是退出了...
throw new RuntimeException("Main thread loop unexpectedly exited");
那么回到我们的问题上,这个死循环会不会导致应用卡死,即使不会的话,它会慢慢的消耗越来越多的资源吗?
对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法
onCreate/onStart/onResume
等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop
本身不会导致应用卡死。
主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到
Linux pipe/epoll
机制,简单说就是在主线程的MessageQueue
没有消息时,便阻塞在loop
的queue.next()
中的nativePollOnce()
方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe
管道写端写入数据来唤醒主线程工作。这里采用的epoll
机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。 Gityuan–Handler(Native层)
Handler连环泡之 说说 Looper 死循环为什么不会导致应用卡死?
事实上,会在进入死循环之前便创建了新binder线程,在代码
ActivityThread.main()
中:
public static void main(String[] args)
//创建Looper和MessageQueue对象,用于处理主线程的消息
Looper.prepareMainLooper();
//创建ActivityThread对象
ActivityThread thread = new ActivityThread();
//建立Binder通道 (创建新线程)
thread.attach(false);
Looper.loop(); //消息循环运行
throw new RuntimeException("Main thread loop unexpectedly exited");
Activity
的生命周期都是依靠主线程的Looper.loop
,当收到不同Message
时则采用相应措施:一旦退出消息循环,那么你的程序也就可以退出了。 从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。
thread.attach(false)
方法函数中便会创建一个Binder线程(具体是指ApplicationThread
,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message
发送给主线程。「Activity 启动过程」
比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()
等方法;
再比如收到msg=H.PAUSE_ACTIVITY
,则调用ActivityThread.handlePauseActivity()
方法,最终会执行Activity.onPause()
等方法。
以上是关于Framework源码面试六部曲:3.Handler面试集合的主要内容,如果未能解决你的问题,请参考以下文章
Framework源码面试六部曲:5.onMeasure测量原理
Framework源码面试六部曲:1.activity启动流程