Handler源码解析:
-
Android源码:
- Android11--->30的源码
-
Handler作用
-
依托消息管理机制,进行线程切换;
-
共享内存池,MessageQueue:避免内存抖动
- next函数:将消息传出去
-
Looper.loop():轮询,取出消息
-
采用for循环不断进行轮询(死循环),取出消息
-
此时,ActivityThread中的main就会在死循环(for(;;))中不断执行
- 因为APP启动后,一直可见(loop中的死循环就保证了代码不会停止)
- 程序不会停下的好处:类似于后台服务器一直保证活(loop是心跳机制)
- 但是当没有消息了,loop就会被阻塞(block)
-
由ActivityThread调用
-
-
调用dispatch函数进行分发:
-
-
APP中的所有事务处理都是Handler
- 每一个APP事务都是Message;
- 服务/Fragment中的每一个生命周期都是一个Message
- 相当于所有代码都是运行在Message之上了
- 证明了所有的异常的log信息的起点都是Looper.loop
-
在处理APP卡顿:定位卡顿位置,判断每个生命周期的执行时间
-
基础:
- Handler对外开放了接口
- 所有主线程中的代码都是Message
-
思路:BlockCanary
- 搞一个日志管理系统:通过日志打印时间不同得出每段生命周期的执行时间,就行了
- 找每个生命周期的执行时间--->找对应的消息的时间
-
执行:BlockCanary的实现思路
- 在Looper中有一个msg.target.dispatchMessage(msg)代码
- 在前后分别整一个开始时间和结束时间--->对logging赋值
- Looper中有一个叫setMessageLogging的接口,设置一个类,就是接口的参数,就可以打印各个生命周期执行的时间
- 最后再在msg.target.dispatchMessage(msg)这行代码后面的还有一个logging对应就行了
-
-
从handler.sendMessage到Handler处理发生了什么?
-
概述:
- sendMessage:向消息队列中添加消息,生产者
- next():从消息队列中取出消息,消费者
- 消息队列本身可以看作仓库,整体为生产者消费者模型
-
生产者--消费者是一个带阻塞的系统
-
当仓库满了--->入队列被阻塞
-
当仓库空了--->出队列被阻塞
-
仓库问题:本身没有设置上限,但是当系统没有内存,进程没有内存时就是满了
-
为什么不设置上限:达到上限APP就挂掉了
- 因为Message本身就是一个心跳机制并且对于60HZ的手机,一秒刷新60次,平均16.7ms刷新一次UI,一次刷新带来3条Message;直接干没了。直接就挂掉了
-
-
仓库什么时候醒来?入队列就醒了
- 会调用Native方法
-
-
什么是阻塞,为什么仓库空了要阻塞,为什么这个阻塞不会导致ANR?
-
什么是阻塞:非得等,并且不能干其他的,阻塞就是睡觉,将CPU的资源放出去;这个时候线程都挂起来了
-
这个时候可以睡觉,当可以了,就会通知你
-
因为安卓中的每一个都可以看做消息,当这个仓库都会空了,那么就让它睡觉,把CPU的资源交出去;
-
这个时候为什么不会ANR,明明线程都挂起来了,而且挂的时间还比较久
-
Handler的阻塞与Message的阻塞两码事并且ANR也算一个消息
-
ANR可以看作一个定时任务没有在定时范围内完成,就像埋了一个定时炸弹;
没有完成就爆炸,并且弹个ANR
-
-
-
Android阻塞是如何实现的:
-
消息队列入队列是如何排序的:for循环轮询,按照执行时间排序,时间早在前面
-
但是当我拿消息(加了锁的Synchronized)的时候,还没有等到它的执行时间,那么就阻塞一秒钟;但这个是不可能的;因为至少系统会不断刷新UI
-
具体实现
-
取出对头消息,拿到时间,计算出需要等的时间
-
退出,将这个时间通过nativePollOnce传到native代码中:
-
java去调用底层代码--->Linux 层;java虚拟机是由C++实现的,
java调用底层代码(JNI 层),此时将这个java代码打上native标记
-
-
什么是静态调用:
- java层中调用的跟Native层中调用的是一样的
-
-
-
Handler中的阻塞:最终是epoll_wait()
-
调用流程:java--->JNI--->CPP--->Linux中的epoll_wait(),此时的阻塞是在底层发起的
-
java层:当消息需要等待的时候调用nativePollOnce(this,等待时间)
-
调用与之同名的JNI层的函数:android_os_MessageQueue_nativePollOnce
-
然后转交到NativeMessageQueue::pollOnce
- 调用C++层中对应的mLooer->pollOnce(等待时间),让Looper等待,因为都干掉Looper了,因为一个线程就只有一个Looper,此时线程的概念就被弱化了
-
再到pollInner:epoll层了
- 最后到这个epoll_wait里面来(在这里线程才被阻塞,上层调用者也因此被阻塞)
-
-
-
非阻塞忙轮询
- 一分钟干一个电话,问到没有
-
场景:设计到I/O:(当有多个网络请求,从数据库中拿数据)
-
阻塞:读数据的时候用的
-
因为在读取数据的时候,由于读取的速度不同(缓存,阻塞)
-
一个线程中如何处理多个I/O?最节约时间的
- 一个线程里面用while循环,遍历所有的流;有就拿,拿完所有的流
- 当所有的流都没有数据了,这个时候还是在for循环里面但是没有读取数据的动作,CPU就空转--->CPU资源浪费
- 所以当流都读完了,就阻塞,避免CPU一直在for循环中挣扎
-
-
引入select:处理的是一系列的流,是一把
- 还有一种解决办法:在for循环之前使用select选择流(Linux 进行实现的),当都没有需要处理的流了,CPU在select里面阻塞;
- select只知道这个一系列流中有I/O需要处理但是它不知道要处理谁,要处理多少个;这个是硬件层面的了
- 一般情况下都是,先使用select判断一下;这一系列中有需要处理的,那么就搞个循环去处理所有的流;select就是先对一大把过滤一下,最后细化
-
细节
- 即使采用多个线程处理多个I/O :由于CPU底层决定了,每一次I/O,都会涉及到硬盘的读取,每一次读取都会降低效率;
-
引入epoll:epoll是Linux内核中的一种可扩展IO事件处理机制。大量应用程序请求时能够获得较好的性能。
-
-
引入epoll
-
工作原理:
- 使用epoll_wait()处理所有的流,当这个流有I/O需要处理的话,就把这个流加到activie_stream[]去--->后面只需要使用一个for循环去遍历activie_stream[]就行了
-
好处:
- CPU底层是需要对所有的流都进行一次遍历O(n),但是引入epoll机制后,将时间复杂度降到O(1)了
-
重要函数:
-
int epoll_create(int size); 创建一个epoll的句柄, size用来告诉内核需要监听的数目一共有多大
在创建MessageQueue的时候,会调用nativeInit,在这个里面完成对epoll进行初始化
-
int epoll_ctl(int epfd, int op, int fd, structepoll_event *event); epoll的事件注册函数,因为MessageQueue本身就可以看做是一个事件
- 整个消息队列就是一个事件;
- 在初始化epoll的时候就直接初始化了这个事件
-
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)
- 这个超时时间是上层传过来的
- 一般不会传-1
-
线程与epoll之间是没有关系的,是在MessageQueue搞了一个出来就会带上一个epoll
-
-
Nginx:基于epoll机制
- 数据在服务器上,通过软件进行数据分发,就是基于epoll机制的;
-
为什么在创建消息队列(int epoll_create(int size);)的时候会调用nativeInit():
- 必然会对epoll进行初始化
-
有了epoll之后,在消息队列的一个构造函数中(MessageQueue(boolean quitAllowed))
-
在调用nativeInit的时候会传入一个epoll的句柄(mPtr):实际上就是一个整数,也是epoll的标签
mPtr = nativeInit(); -
在调用nativePollOnce(ptr,nextPollTimeoutMillis);
- 就是向ptr句柄指向的epoll中,等了那么久的时间:nextPollTimeoutMillis;
-
nativeWake(ptr):向消息队列中放入一个消息时会调用此函数
- ptr指向具体的epoll对应的事件
- ptr不是地址值,内部是有封装的,只是说代表函数
- 具体的唤醒是在Linux层中的字节码
- epoll是在IO中管理阻塞的机制
-
-
同步屏障:保障UI刷新(此事件优先级最高)
-
概述:一般是按照消息队列的顺序进行执行的,就是交警,先封路再解封
-
场景:120HZ 的屏幕,3ms发送Message更新UI,但是此时的消息队列是按照顺序执行的,怎么去保障UI正常刷新;
-
postSyncBarrier:就是屏障,就是Message,target为null的就是屏障消息
-
创建了一个msg,然后直接干到队列中;
-
一把的消息,在创建消息后,会对target属性进行赋值的
-
一般的消息(要执行的消息):同步消息(自己调用的),异步消息(需要优先执行的,比如UI刷新(在ViewRootIml中)),(Message共有三种,同步,异步,屏障)
-
UI刷新:ViewRootIml
-
在scheduleTraversals()中
- 先发送一个屏障消息:mTraversalsBarrier = xxx.postSyncBarrier;
-
调用postCallBack:调用到一个Internel,这个都调用到了choreographer:UI刷新的源头,60H在,120HZ都是在这里决定的
-
postCallBackDelayedInternal中的else
-
发送了一个Message:UI更新的Message
-
设置异步属性:msg.setAsynchronous(true)
- 所以这个消息是异步消息,一般的消息是不会设置这个属性的
-
-
-
就有个异步消息发送
-
-
-
取出消息:从队头去取出来,卡顿优化;
-
场景:当队头消息为onResume函数,已知其耗时200ms,
-
效果:画面卡顿,在log中提示:fragme丢失
- Skipped 30 frames! The application may be doing too much work on its main thread. 因为在执行这个消息的时候,有30次UI刷新没有正常进行;
- 怎么去解决这个问题:用Loop里面的log去追,看看是那个耗时了,抓到了就解决它;
- 抓到他了,实在不能优化,就放到异步线程里面去
- 产生的原因:因为队列前的消息太耗时了,即使存在同步屏障机制,但是还是需要等到这个耗时消息执行完后,当准备去执行异步消息的时候,队列中都卡了30个异步消息了 因为消息队列为生产者-消费者模式,消费和生产是可以同时进行的,完全存在异步消息堆积的可能
-
细节:不会发生ANR,一般情况,应用无响应都是5秒
-
-
那么就需要保证优先级高的消息进行执行:
-
例如:scheduleTraversals,先设置屏障,调用postCallBack,在里面为此消息设置异步属性;流程就是这样
-
具体流程:
-
设置屏障:mTraversalsBarrier = xxx.postSyncBarrier;
- 发送了一个屏障消息
-
发送需要更新的事件:异步消息
-
-
细节一:设置屏障的时候mTraversalsBarrier = xxx.postSyncBarrier;
- 发送了一个屏障消息
-
为什么屏障要在异步消息之前:因为屏障消息需要先执行
-
同时,在取消息(执行next):会有一个if条件判断,当msg.target==null(说明其为屏障消息)
-
此时执行do-while循环:轮询队列
- 找到异步消息:msg.isAsynchronous()&&msg!=null
- 执行异步消息:返回异步消息
- 返回Loop里面去处理:Message msg = queue.next()
-
-
当处理完整个异步消息之后,有一个unscheduleTraversals函数:
- removeSyncBarrier:移除屏障
- 这个removeCallBack里面:remove这个刷新的消息
-
-
不移除屏障消息会发生什么
-
会一直在哪个for循环里面:MessageQueue里面有个for(;;)
- 因为它发现每次队头消息都是屏障消息:每次都会执行相同的代码
-
-
屏障消息只能插队头吗?
-
所有的消息都是有时间顺序的:
-
比如插入屏障消息:postSyncBarrier(long when){}when就是时间,当前时刻
-
内部代码:屏障消息也是基于时间排序的
if(when != 0){ xxx }
-
-
屏障消息本质上也是一个消息,也是按照时间顺序进行入队和出队,只是说,它的target属性为null而已
-
-
为什么每次UI更新至少要刷三个消息
- 三个消息:屏障消息,UI刷新(异步消息),解决屏障消息(一般消息)
-
scheduleTraversals:这个是开放的接口
-
执行doTraversals:
-
执行performTraversals:
-
里面就有三个重要的函数:
- performDraw
- perforLayout
- performMeasure:测量的起点,从根布局进行测量
-
-
UI 是一颗树形的,有嵌套关系,UI的刷新,实质上就是刷新根布局
-
Handler源码解析(一)
-
源码的问题:理解源码为什么去实现,为什么去用,会造成什么效果
-
Handler存在的意义:
- 系统的核心
- 跟web中的AJAX有异曲同工之妙,Spring架构中有
- 大大降低了并发的问题出现的可能性,几乎看不到多线程死锁的问题;
-
数据通信带来什么问题
-
线程如何通讯:
- 方法:LiveData,EventBus,采用Handler通信(共享内存)
-
为什么线程之间不会干扰:Handler使用锁机制,内存管理设计优秀
-
为什么wait/notify用武之地不大:
-
java是基于虚拟机的,虚拟机是用C/C++进行实现的
-
handler已经将这部分功能进行了Linux层的封装
- Linux的epoll机制
-
-
-
为什么Handler通信是共享内存
-
课程内容:
- 源码
- 设计思路
- 设计模式
- 异步消息,同步消息,消息屏障
- HandlerThread
- IntentServer
-
Choreographer:编舞者
- 管理事件分发,处理屏幕点击,将点击事件封装成一个消息
-
Handler:实现线程(子线--->主线;主线--->子线)间通信;
- 是消息管理机制,是系统的维护者:ActivityThread(AMS中的一个部分,就是围绕Handler玩的)
- 子线程(携带javaBean)--->主线程(显示)
- 一般情况下都是子线程发送消息,主线程接收消息
-
应用的启动:
-
lancher(桌面程序,是一个app):启动app的,
- 点击应用图标就相当于点击了lancher的一个按键
- 进行响应,zygot,fork出一个进程给应用(一对一的),
-
zygot:
-
fork出一个进程给应用(一对一的),为每一个分配独立的JVM
-
JVM中的java程序,那么java程序就有main函数,JVM就启动这个main函数,程序就开始运行,这个main函数就在ActivityThread.java里面
-
zygot为什么要给每一个程序分配一个虚拟机
- 主要就是隔离
- 独立的,挂掉了 ,不会影响其他的应用
- 在功能机中出现FATAL ERROR,就是一个程序挂掉,那么手机就挂掉了,智能机就不会
-
main函数中干了什么:
- 环境(JVM)初始化
Environment.initForCurrentUser();-
打印log
-
启动主线程的Loop:主线程就运行起来了(准备了一个主线程的Loop,Looper,prepareMainLooper();,执行这个Loop:Looper.loop())
-
Looper.loop():意味着APP所有的代码都是Handler管理的,APP的所有代码都是在Handler中进行的
-
有一个死循环:代码不能跳出循环,那么Handler只需要不停的分发消息进行
-
就是一个永动机,不断在消息队列中进行
-
除非Loop断开:返回一个为空的msg
-
什么时候返回一个为空的msg:
- 应用退出:调用quit,就是MessageQuit
-
-
Handler:
-
所有的代码都在Handler之中运行
-
线程通信只是它的一个附属功能而已
-
运行起来就是一个传送带,
-
-
-
-
-
Handler--->sendMeassage--->message.enqueMessage()--->handler.handMessage()处理消息
-
源码不能打断点,除非自己编译一个模拟器
-
有一张图:
-
enqueMessage:
- 功能:将消息放到消息队列中去,看源码的时候就可以看到明显的队列痕迹(优先队列,单链表构成的,不会冲突)
-
Next:从消息队列中取消息,返回一个Message
-
由Loop调用,Loop来取:Looper.loop
- 有一个死循环,去调用next()函数
- 在调用之前,传送带就在滚动了
- 再调用dispatch函数
-
-
Handler的调度流程:
-
handler--->sendMessage--->messageQueue.enqueMessage(插入消息队列)--->looper.loop()--->messageQueue.next()从消息队列中取出消息--->handler.dispatchMessage()--->handler.handleMessage()
-
loop:通上电,就一直跑;在for循环中一直跑
-
message就在滚动:
- 因为message代表一个内存,就相当于是内存共享了
- message:new,obtain
- 因为内存又是不分线程区别的,所以可以共用
-
message是怎么从子线程到主线程的
- 假设在子线程中进行一个handler.sendEmptyMessage(),在主线程调用handleMessage()进行消息处理;这个就实现了从子线程到主线程的切换
-
-
消息队列是一个什么样的数据结构
-
采用单链表实现的优先级队列
-
具体实现:为什么是单链表
- 在MessageQueue持有一个Message对象mMessage
- 在Message对象中有一个Message next,这个next也是一个Message;
- 依托于这种嵌套机制,就实现了一个链表,并且还是单链表
-
具体实现:为什么具有优先级?
-
sendXxxMessageAtTime()
-
在MessageQueue.java中有一个enqueMessage()函数
-
插入排序
-
-
消息的when属性是怎么来的?在Handler.java中的sendXXXMessage()里面的
-
它都是一个单链表了,那么为什么是队列?
-
MessageQueue.java中的next()函数
-
-
在MessageQueue构造的时候使用了插入排序进行操作的
-
-
Loop源码:
-
核心:构造函数,loop(),ThreadLocal
-
Loop是怎么初始化的?
-
Looper.java中有一个私有的构造函数
private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); } -
既然是私有的构造函数,那么怎么初始化?在Looper.java中有一个prepare函数
-
为什么要在prepare里面这样去整个私有的构造去耍?因为在ActivityThread.java里的main函数中:prepareMainLooper
-
首先:当这个Looper的构造函数是public,那么谁都可以new,就不好管理
-
进行判断:
-
-
-
ThreadLocal:多线程中进行线程上下文的存储变量;
-
源码:这个源码要去补充
-
就是线程隔离的工具类,本身是不存数据的,在调用set函数,根据所需要存储的数据获取当前的线程,每一个线程里面都有一个ThreadLocalMap(每一个线程的一个成员变量,用于存储相应线程的上下文),这个Map是一个弱应用的东西,key就是ThreadLocal,Value就是所需要保存的数据
-
ThreadLocal.java中的set
-
Looper.java
-
保证了一个线程就只有一个Looper,并且当Looper存在后就不能改了
- 因为一个线程对应一个ThreadLocalMap,
- 中间有个ThreadLocal做了限制的
- ThreadLocalMap<唯一的ThreadLocal,value>,因为this唯一,退出value唯一;因为键值对是一一对应的
-
怎么去保证ThreadLocal,在value活着的时候,只跟value在一起?
- 在HashMap的源码中,对一个key不断set,会导致value覆盖的情况;只需要只让他set一次就行了 ;那么就在那个prepare()中做手脚
- looper.java中的prepare函数:

-
MessageQueue在主线程还是子线程?
-
Handler.java 中的Handler构造函数
-
Looper由ThreadLocal中来:Looper会维持一个MessageQueue;
-
MessageQueue伴随Looper创建,两者在一个线程中均是唯一
-
多个线程,会有多个Message吗?
- 不好说,因为一个线程只会创建一个Looper,一个Loop只会创建一个MessageQueue
- 那么在主线程中,就只能是线程,Looper,MessageQueue一一对应;
- 关键就是在于看有没有在多个线程中创建多个Looper
-
为什么要有ThreadLocal?
- 保证一个线程里面只有一个Looper
-
面试题:
-
一个线程中有几个Handler
- 无数个,Handler机制就只有一个
- 因为Handler是可以new出来的,想整几个整几个
- 怎么去new?
-
一个线程有几个 Looper?怎么保证?
-
就只有一个,用了一个ThreadLocal来保证
-
ThreadLocal中的prepare中检验
- 如何说不为空,就证明已经有了,抛出异常
-
-
Handler内存泄漏的原因?为什么其他内部类就没有?
-
内部类持有外部类的对象:java编程思想中的有(2-7章)
-
recycleView中的adapter,ViewHolder这些都是内部类为什么没有内存泄漏?
- 因为生命周期决定了
-
源码分析:handler中的sendMessageAtTime中调用enqueueMessage,在enqueueMessage中
-
所以在Message的传递过程中,可以set一个Message在20分钟后执行,那么这个message会等待20分钟,并且在这20分钟中,这个message一直持有这段内存20分钟;并且message持有了Handler,Handler又持有了Activity,并且在这是GC是不能被回收的,因为message不可达,那么他持有的所有东西都不能被回收;因为这种关系,在内部类持有外部类中,这个内部类在外部类的生命周期的范围被别的对象持有了,外部类也不能释放;这个就是内存泄漏的真正原因
-
-
为何主线程可以new Handler,在子线程中怎么new Handler?
- 主线程本来就可以new,在主线中的ActivityThread()在main函数中就弄了下面的两句
- 子线程中必须干两句:先Looper.prepare()再Looper.loop()不然运行不了,就算new出来的 Handler没有什么调用;
-
子线程中维护的Looper,消息队列没有消息的时候处理方案是什么?有什么用?
-
quit函数:Looper.java中的
-
Loop函数是一个死循环,退出的条件是msg==null,但是msg又不能等于null,只有在Looper.java中的quit中,调用了MessageQueue.java中的quit函数,此时对mQuitting赋值为true,将消息队列中的所有消息全部清空,之后,进行nativeWake(mPtr)唤醒
-
MessageQueue的消息睡眠与唤醒
- 生产者-消费者模型
- 在子线程中enqueueMessage向消息队列中添加消息
- 在主线程中调用next从消息队列中取出消息;
- 所有的消息都在MessageQueue,仓库(这个是可以挂掉的,比如说作一个秒表,后台进行notification,造成整个消息队列中全是Message,Message并不适合定时功能,用System.clock就行了)
- 当队列满或者空,就会阻塞;
-
MessageQueue的阻塞机制
-
没有限制的,随便整就行了,最多就阻塞;Handler不做受限,内存耗完就G了
-
在java多线程中有个阻塞队列,多了,后面的就阻塞
-
为什么Handler不设置上限?
- Handler是大家都在用的,系统还是在用这个,系统消息进不来就G了
-
两个方面阻塞:
-
取出的消息还没有当它的执行时刻阻塞,自动唤醒,时间够了就将这个消息return出去
-
因为没有调用quit函数,还是在这个for(;;)里面,调用nativePollOnce
-
nativePollOnce:注意第2个参数(-1,无限等待,直到有消息来将其唤醒,0,不等待,值就是等待的时间)
-
第二层等待:消息队列为空的时候,还是会调用nativePollOnce,无限等待的状态;只有向消息队列中添加一个消息,进行唤醒就是调用nativeWake(ptr)
-
因为ptr的默认值是-1,相当于待机
-
- Handler没有做队列满的操作,内存耗尽就满了,就G了
-
-
nativeWake怎么玩的?
- java的wake--->JNI 同名函数--->native的wake(mLooper->wake())--->C++的wake--->Linux中的wake
- 这个是NDK的了
-
阻塞是否提升了性能?
- 对,空出了CPU;
-
-
当消息队列没有消息,是怎么处理的?
-
主线程:Looper.java中的loop函数
-
子线程中:
-
调用quit函数,Looper.quit--->MessageQueue.quit
-
对那个东西进行赋值,调用nativeWake,唤醒后,往下执行
-
直接返回null:此时的msg就是null:MessageQueue里的next
-
然后再次检验就退出了Looper.loop
-
-
-
-
既然可以存在多个Handler向MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那么如何保证线程安全?
-
依托锁机制,在MessageQueue中的enqueueMessage基本被synchronized包裹了
-
这个东西叫内置锁:依托JVM实现上锁,解锁;可以锁代码块
-
为什么里面用this,锁的就是所有的东西,锁了对象,对象里面所有的函数、代码块等全部都会被受限;那么当我在执行synchronized(this)代码块中的内容时,同一对象其他地方上this锁的地方都不能走,全部都要等;但是,锁只是锁的对象;对象都不同了,随便整;但是对象相同了,就不行,就像调用了enqueueMessage就不能调用next
-
一个线程只有一个可以操作MessageQueue的地方
- 因为一个线程只有一个MessageQueue对象,对象里面又有锁
-
当多个Handler处于不同线程同时向消息队列中放消息
- 排序,谁早,先插谁,因为上锁了的
- 为什么取消息的时候要加锁,因为你取的时候也有人在插入
- 退出的时候也要加锁,退出了就不要插,取
-
主线程不能调用quit,这个调用就崩溃了
-
-
Handler面试解析
-
Handler工作流程
-
示意图:
-
具体流程:
- 整体为一条传送带,调用postXxx函数或者sendXXX函数将消息放到传送带(MessageQueue优先级队列),Looper.loop函数为其提供动力(其中for(;;)中不断轮询消息队列,调用next函数取出消息);拿出队头消息(执行时间最早),当消息满足执行条件(当前时间大于消息的执行时间)调用dispatch进行消息分发;一般来说是从子线程将消息放入队列,主线程接收消息队列中的消息,实现了线程之间通信;每一条Message对应了一段内存区域,整体实现了共享内存
-
疑问:线程之间的跨越是如何实现的?
-
内存是没有线程概念的:
- 比如在Activity中可以new一个HashMap,此时开启线程,在线程中也可以使用这个HashMap
-
-
为什么发送的是子线程,接收消息是主线程?
- 子线程中执行的函数,那么这个函数就属于子线程
- 一般是在异步线程中发送消息:handler.sendXXXMessage(msg)--->MessageQueue.enqueueMessage(msg)
- 只是说将子线程中需要执行的事件,转成了一段内存,
- 一个线程有一个独立的ThreadLocal,一个ThreadLocal有唯一对应的Looper,对应MessageQueue(唯一)
- 主线程中的Looper.loop()一直在执行,轮询主线程中的MessageQueue,那么loop函数在主线程中调度,那么其中的for(;;)就在主线程中调度,for(;;)其中的,msg.target.dispatchMessage(msg)就在主线程中了;
-
-
ThreadLocal的唯一性:
-
static final修饰了ThreadLocal,这个就是唯一的,
- 意味着所有的线程都是只有这一个,全局唯一的
-
Looper:线程单例;
-
全局只有一个Looper?:线程唯一的
-
ThreadLocal的全局唯一性怎么保障Looper的线程单例
- 每一个线程都有各自的ThreadLocalMap
- 并且ThreadLocalMap的KEY都是唯一的:ThreadLocal
- 那么他的Value就肯定只有一个了
-
-
handler细节:
-
享元模式:
-
当消息成功执行后:调用 msg.target.dispatchMessage(msg);
-
会调用msg.recycleUnchecked();将这个消息存起来;
-
将Message所有的内容全部置空
-
将这个置空的消息放到sPool(也是一个消息)里面去
-
构成了一个新的回路,相当于将其查到队头去
- 避免了抖动的问题:防止OOM
- 因为new 的时候会造成内存碎片,因为new的时候是开辟的是连续的内存空间
- 内存碎片就会导致OOM
- Full GC会回收内存,但是不能保证一定回收了;
- BitMap也是这样玩的,不用了,就把数据擦掉;后面就直接用
-
-
设计:
- Activity管理,intent,BitMap等都是这种享元设计模式;达到内存复用的目的
- 还有在RecycleView里面就有;
-
-
Looper的死循环为什么不会导致ANR
-
ANR:点击事件5S没有响应(没有处理完),一般10S广播没有响应,就出现ANR,Service20S
-
点击事件最终还是一个Message,封装在Choreographer中的doFrame里面就会将点击事件等封装成消息,调用CallBack()
-
发送消息之后,就记录一个时间
-
消息执行后,执行doCallBacks():这里检测到执行时间大于5S(对于点击事件哈),那么让Handler发送一个ANR的提醒
-
Handler发送ANR跟Looper里面的block没有关系
-
Handler会什么会弹出弹窗,因为这个Handler可以自己唤醒自己,并且优先级高
-
-
-
为什么Looper.block不会导致ANR?
- Looper.block只是交出CPU而已,此时线程没有事情干,根本就没有消息
-
-
HandlerThread存在的意义:在子线程中创建一个属于自己的Looper
-
优点:
- 方便使用:方便初始化,方便获取线程looper
- 保证了线程安全
-
如果自己去封装:不行,在多线程背景下,可能子线程中的东西都没有去执行,异步问题
Looper looper; public void run(){ 开一个子线程 looper.prepare() //初始化handler threadHandler = new Handler(loop) looper.loop() } thread.start() Handler handler = new Handler(thread.getLooper()) -
多线程的锁机制:Handler解决了锁机制问题
-
不用HandlerThread东西:
开一个子线程 looper.prepare() //初始化handler threadHandler = new Handler(loop) looper.loop()千万不能将这个Looper搞成全局的,引起内存的问题
-
在主线程中是不能创建handler的,不行的
-
HandlerThread怎么玩的?
-
继承了Thread,本身就是一个子线程,在哪里都是可以用的
-
对Looper进行了封装
-
getLooper执行细节:notifyAll与wait处理的是同一个锁
-
-
-
IntentService:
-
Service:
- 处理耗时的后台应用,在其中启动一个线程,去处理耗时任务;
- 生命周期由后台处理,那么IntentService生命周期后台调度
-
IntentService.onCreate
-
示意图:
-
-
IntentService.onHandleIntent
-
示意图:
-
-
IntentService.onStart()
-
示意图:
-
-
IntentService.SerViceHandler
-
示意图:
-
所以只需要在new出IntentService的子类,实现onHandleIntent,将耗时工作放到这个里面就行了
-
-
-
Handler源码有什么用:Handler处理完异步耗时操作之后,自己就把自己干掉了
-
从handleMessage开始:这个里面是在子线程执行消息,子线程中维护了一个消息队列
-
消息处理完后执行:stopSelf(msg.arg1);是Service的stopSelf
处理完Service自动停止,不用担心内存还要泄漏,自己就干掉自己了
-
为什么只有一个Message,发了很多个消息?
-
场景:一项任务分成几个子任务,子任务全部执行完后,这个任务才成功;这样子就可以用IntentSetvice来解决,最佳方案;
-
解决:IntentService
用多个线程来处理,一个线程处理完--->下一个线程--->下一个线程
-
IntentService优势:维持一个线程独有的队列,可以保证每一个任务都是在同一个线程中去处理,并且都是依次执行。
-
怎么保证这些都是在同一个线程中?
-
因为获取到的都是同一个线程对应的Looper,
- 因为在IntentService里面的onCreate中创建的是HandlerThread

-
HandlerThread里面的Looper就是这个线程独有的,
-
一个线程只有一个Looper,这样就保证了就是一个线程
一个线程对应一个ThreadLocalMap---><唯一的ThreadLocal,value> looper对应唯一的MQ
-
-
怎样保证它是线程中的任务时顺次执行的
- 任务就是消息,放到消息队列中的,这个是有先后顺序的,一定是执行前面的,再来后面的,保证线程任务的依次执行
-
多个任务用同一个IntentService就行了
-
IntentService怎么用的?
-
Service还要在清单文件中注册;
-
-
-
原理的其他应用场景:
-
Fragment的生命周期管理:FragmentTabPagerAdapter.java
- Glide的生命周期中的应用:在Handler源码面试总结的这个地方没有听
-
-
Glide的生命周期使用:后面去补上这个问题;
- Glide.with