换个姿势,带着问题深入学习Handler,手撕吊打面试官!
原文:coder-pig https://juejin.im/post/6844904150140977165Handler,老生常谈,网上关于它的文章也是“泛滥成灾”,而实际开发中,
我们却很少手写Handler,毕竟,RxAndroid链式调用 或者 Kotlin协程同步方式 写异步代码 不香么?不过,面试官都喜欢章口就来一句:
文章图片
应对方法也很简单:找一篇《Handler详解》类的文章,背熟即可,
不过,对于我这种好刨根问底的人来说,自己过一遍源码心理才踏实,
而且,我发现「带着问题」看源码,思考理解本质,印象更深,收获更多,遂有此文。
罗列下本文提及的问题,有答不出的可按需阅读,谢谢~
- 1、Handler问题三连:是什么?有什么用?为什么要用,不用行不行?
- 2、Android UI更新机制(GUI) 为何设计成了单线程的?
- 3、真的只能在主(UI)线程中更新UI吗?
- 4、真的不能在主(UI)线程中执行网络操作吗?
- 5、Handler怎么用?
- 6、为什么建议使用Message.obtain()来创建Message实例?
- 7、为什么子线程中不可以直接new Handler()而主线程中可以?
- 8、主线程给子线程的Handler发送消息怎么写?
- 9、HandlerThread实现的核心原理?
- 10、当你用Handler发送一个Message,发生了什么?
- 11、Looper是怎么拣队列里的消息的?
- 12、分发给Handler的消息是怎么处理的?
- 13、IdleHandler是什么?
- 14、Looper在主线程中死循环,为啥不会ANR?
- 15、Handler泄露的原因及正确写法
- 16、Handler中的同步屏障机制
- 17、Android 11 Handler相关变更
文章图片
BAT面试题集合/面经免费获取:【github】
一、Handler问题三连 1.Handler是什么 答:Android Framework 架构中的一个 基础组件,用于 子线程与主线程间的通讯,实现了一种 非堵塞的消息传递机制。
2.Handler有什么用 答:把子线程中的 UI更新信息传递 给主线程(UI线程),以此完成UI更新操作。
3.为什么要用Handler,不用行不行 答:不行,Handler是android在设计之初就封装的 一套消息创建、传递、处理机制。
Android要求:
在主线程(UI)线程中更新UI是要求,建议,不是规定,你不听,硬要:
在子线程中更新UI,也是可以的!!!比如,在一个子线程中,创建一个对话框:
文章图片
运行后:
文章图片
没有报错,对话框正常弹出,而我们平时在 子线程中更新UI 的错:
文章图片
异常翻译:只有创建这个view的线程才能操作这个view;
引起原因:在子线程中更新了主线程创建的UI;
也就是说:子线程更新UI也行,但是只能更新自己创建的View;
换句话说:Android的UI更新(GUI)被设计成了单线程;
你可能会问,为啥不设计成多线程?
答:多个线程同时对同一个UI控件进行更新,容易发生不可控的错误!那么怎么解决这种线程安全问题?
答:最简单的 加锁,不是加一个,是每层都要加锁(用户代码→GUI顶层→GUI底层…)这样也意味着更多的 耗时,UI更新效率降低;如果每层共用同一把锁的话,其实就是单线程。所以,结论是:
Android没有采用「线程锁」,而是采用「单线程消息队列机制」,实现了一个「伪锁」这个疑问解决了,再说一个网上很常见的主线程更新UI的例子:
文章图片
上面这段代码 直接在子线程中更新了UI,却没有报错:
文章图片
这是要打脸吗?但如果在子线程中加句线程休眠模拟耗时操作的话:
文章图片
程序就崩溃了,报错如下:
文章图片
前面说了 Android的UI更新被设计成单线程,这里妥妥滴会报错,但却发生在延迟执行后?限于篇幅,这里就不去跟源码了,直接说原因:
ViewRootImp 在 onCreate() 时还没创建;可以打个日志简单的验证下:
在 onResume()时,即ActivityThread 的 handleResumeActivity() 执行后才创建;
调用 requestLayout(),走到 checkThread() 时就报错了。
文章图片
加上休眠:
文章图片
行吧,以后去面试别人问「子线程是不是一定不可以更新UI」别傻乎乎地点头说是了。
4.引生的另一个问题 说到「只能在主线程中更新UI」我又想到另一个问题「不能在主线程中进行网络操作」
文章图片
上述代码运行直接闪退,日志如下:
文章图片
NetworkOnMainThreadException:网络请求在主线程进行异常。
em… 真的不能在主线程中做网络操作吗?在 onCreate() 的 setContentView() 后插入下面两句代码:
文章图片
运行下看看:
文章图片
这…又打脸?先说下 StrictMode(严苟模式)
Android 2.3 引入,用于检测两大问题:ThreadPolicy(线程策略) 和 VmPolicy(VM策略)相关方法如下:
文章图片
把严苟模式的网络检测关了,就可以 在主线程中执行网络操作了,不过一般是不建议这样做的:
在主线程中进行耗时操作,可能会导致程序无响应,即 ANR (Application Not Responding)。至于常见的ANR时间,可以在对应的源码中找到:
// ActiveServices.java → Service服务static final int SERVICE_TIMEOUT = 20*1000;
// 前台static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
// 后台// ActivityManagerService.java → Broadcast广播、InputDispatching、ContentProviderstatic final int BROADCAST_FG_TIMEOUT = 10*1000;
// 前台static final int BROADCAST_BG_TIMEOUT = 60*1000;
// 后台static final int KEY_DISPATCHING_TIMEOUT = 5*1000;
// 关键调度static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10*1000;
// 内容提供者
时间统计区间:
二、Handler怎么用 1.sendMessage() + handleMessage() 代码示例如下:
- 起点:System_Server 进程调用 startProcessLocked 后调用 AMS.attachApplicationLocked()
- 终点:Provider 进程 installProvider及publishContentProviders 调用到 AMS.publishContentProviders()
- 超过这个时间,系统就会杀掉 Provider 进程。
文章图片
黄色部分会有如下警告
文章图片
Handler不是静态类可能引起「内存泄露」,原因以及正确写法等下再讲。
另外,建议调用 Message.obtain() 函数来获取一个Message实例,为啥?点进源码:
文章图片
从源码,可以看到obtain()的逻辑:
Message池复用Message,可以「避免重复创建实例对象」节约内存,
- 加锁,判断Message池是否为空
- ① 不为空,取一枚Message对象,正在使用标记置为0,池容量-1,返回;
- ② 为空,新建一个Message对象,返回;
另外,Message池其实是一个「单链表结构」
文章图片
上述获取消息池的逻辑:
文章图片
定位到下述代码,还可以知道:池的容量为50,超过
文章图片
然后问题来了,Message信息什么时候加到池中?
答:当Message 被Looper分发完后,会调用 recycleUnchecked()函数,回收没有在使用的Message对象。
文章图片
标志设置为FLAG_IN_USE,表示正在使用,相关属性重置,加锁,判断消息池是否满,
未满,「单链表头插法」把消息插入到表头。(获取和插入都发生在表头,像不像 栈~)
2.post(runnable) 代码示例如下:
文章图片
跟下post():
文章图片
实际上调用了 sendMessageDelayed() 发送消息,只不过延迟秒数为0,
那Runnable是怎么变成Message的呢?跟下getPostMessage()
文章图片
噢,获取一个新的Message示例后,把 Runnable 变量的值赋值给 callback属性
文章图片
3.附:其他两个种在子线程中更新UI的方法 activity.runOnUiThread()
文章图片
view.post() 与 view.postDelay()
文章图片
三、Handler底层原理解析 终于来到稍微有点技术含量的环节,在观摩源码了解原理前,先说下几个涉及到的类。
1.涉及到的几个类
文章图片
2.前戏 在我们使用Handler前,Android系统已为我们做了一系列的工作,其中就包括了
创建「Looper」和「MessageQueue」对象上图中有写:ActivityThread 的 main函数是APP进程的入口,定位到 ActivityThread → main函数
文章图片
定位到:Looper → prepareMainLooper函数
文章图片
定位到:Looper → prepare函数
文章图片
定位到:Looper → Looper构造函数
文章图片
另外这里的 mQuitAllowed 变量,直译「退出允许」,具体作用是?跟下 MessageQueue:
文章图片
em…用来 防止开发者手动终止消息队列,停止Looper循环。
3.消息队列的运行 前戏过后,创建了Looper与MessageQueue对象,接着调用Looper.loop()开启轮询。
定位到:Looper → loop函数
文章图片
接着有几个问题,先是这个 myLooper() 函数:
文章图片
文章图片
这里的 ThreadLocal → 线程局部变量 → JDK提供的用于解决线程安全的工具类。
作用:为每个线程提供一个独立的变量副本 → 以解决并发访问的冲突问题。
本质:
每个Thread内部都维护了一个ThreadLocalMap,这个map的key是ThreadLocal,主线程和子线程的Looper对象实例相互隔离的!!!
value是set的那个值。get的时候,都是从自己的变量中取值,所以不存在线程安全问题。
意味着:主线程和子线程Looper不是同一个!!!
知道这个以后,有个问题就解惑了:
「为什么子线程中不能直接 new Handler(),而主线程可以?」
答:主线程与子线程不共享同一个Looper实例,主线程的Looper在启动时就通过
prepareMainLooper() 完成了初始化,而子线程还需要调用 Looper.prepare()
和 Looper.loop()开启轮询,否则会报错,不信,可以试试:
文章图片
直接就奔溃了~
文章图片
加上试试?
文章图片
可以,程序正常运行,没有报错。
对了,既然说Handler用于子线程和主线程通信,试试在主线程中给子线程的Handler发送信息,修改一波代码:
文章图片
运行,直接报错:
文章图片
原因:多线程并发的问题,当主线程执行到sendEnptyMessage时,子线程的Handler还没有创建。
一个简单的解决方法是:主线程延时给子线程发消息,修改后的代码示例如下:
文章图片
运行结果如下:
文章图片
可以,不过其实Android已经给我们封装好了一个轻量级的异步类「HandlerThread」
4.HandlerThread HandlerThread = 继承Thread + 封装Looper
使用方法很简单,改造下我们上面的代码:
文章图片
用法挺简单的,源码其实也很简单,跟一跟:
文章图片
文章图片
文章图片
剩下一个quit()和quitSafely()停止线程,就不用说了,所以HandlerThread的核心原理就是:
是吧,HandlerThread的实现原理竟简单如斯,另外,顺带提个醒!!!
- 继承Thread,getLooper()加锁死循环wait()堵塞线程;
- run()加锁等待Looper对象创建成功,notifyAll()唤醒线程;
- 唤醒后,getLooper返回由run()中生成的Looper对象;
Java中所有类的父类是 Object 类,里面提供了wait、notify、notifyAll三个方法;代码示例如下:
Kotlin 中所有类的父类是 Any 类,里面可没有上述三个方法!!!
所以你不能在kotlin类中直接调用,但你可以创建一个java.lang.Object的实例作为lock,
去调用相关的方法。
private val lock = java.lang.Object()
fun produce() = synchronized(lock) {
while(items>=maxItems) {
lock.wait()
}
Thread.sleep(rand.nextInt(100).toLong())
items++
println("Produced, count is$items:${Thread.currentThread()}")
lock.notifyAll()}fun consume() = synchronized(lock) {
while(items<=0) {
lock.wait()
}
Thread.sleep(rand.nextInt(100).toLong())
items--
println("Consumed, count is$items:${Thread.currentThread()}")
lock.notifyAll()
}
5.当我们用Handler发送一个消息发生了什么? 扯得有点远了,拉回来,刚讲到 ActivityThread 在 main函数中调用 Looper.prepareMainLooper
完成主线程 Looper初始化,然后调用 Looper.loop() 开启消息循环 等待接收消息。
嗯,接着说下 发送消息,上面说了,Handler可以通过sendMessage()和 post() 发送消息,
上面也说了,源码中,这两个最后调用的其实都是 sendMessageDelayed()完成的:
文章图片
第二个参数:当前系统时间+延时时间,这个会影响「调度顺序」,跟 sendMessageAtTime()
文章图片
获取当前线程Looper中的MessageQueue队列,判空,空打印异常,否则返回 enqueueMessage(),跟:
文章图片
这里的 mAsynchronous 是 异步消息的标志,如果Handler构造方法不传入这个参数,默认false:
这里涉及到了一个「同步屏障」的东西,等等再讲,跟:MessageQueue -> enqueueMessage
文章图片
如果你了解数据结构中的单链表的话,这些都很简单。
6.Looper是怎么拣队列的消息的? MessageQueue里有Message了,接着就该由Looper来分拣了,定位到:Looper → loop函数
// Looper.loop()
final Looper me = myLooper();
// 获得当前线程的Looper实例
final MessageQueue queue = me.mQueue;
// 获取消息队列
for (;
;
) {// 死循环
Message msg = queue.next();
// 取出队列中的消息
msg.target.dispatchMessage(msg);
// 将消息分发给Handler
}
queue.next() 从队列拿出消息,定位到:MessageQueue -> next函数:
文章图片
这里的关键其实就是:nextPollTimeoutMillis,决定了堵塞与否,以及堵塞的时间,三种情况:
等于0时,不堵塞,立即返回,Looper第一次处理消息,有一个消息处理完 ;Tips:此处用到了Linux的pipe/epoll机制:没有消息时阻塞线程并进入休眠释放cpu资源,有消息时唤醒线程;
大于0时,最长堵塞等待时间,期间有新消息进来,可能会了立即返回(立即执行);
等于-1时,无消息时,会一直堵塞;
7.分发给Handler的消息是怎么处理的? 通过MessageQueue的queue.next()拣出消息后,调用msg.target.dispatchMessage(msg)
把消息分发给对应的Handler,跟到:Handler -> dispatchMessage
文章图片
到此,关于Handler的基本原理也说的七七八八了~
8.IdleHandler是什么? 评论区有小伙子说:把idleHandler加上就完整了,那就安排下吧~
在 MessageQueue 类中有一个 static 的接口 IdleHanlder
文章图片
翻译下注释:当线程将要进入堵塞,以等待更多消息时,会回调这个接口;
简单点说:当MessageQueue中无可处理的Message时回调;
作用:UI线程处理完所有View事务后,回调一些额外的操作,且不会堵塞主进程;
接口中只有一个 queueIdle() 函数,线程进入堵塞时执行的额外操作可以写这里,
返回值是true的话,执行完此方法后还会保留这个IdleHandler,否则删除。
使用方法也很简单,代码示例如下:
文章图片
输出结果如下:
文章图片
看下源码,了解下具体的原理:MessageQueue,定义了一个IdleHandler的列表和数组
文章图片
定义了添加和删除IdleHandler的函数:
文章图片
在 next() 函数中用到了 mIdleHandlers 列表:
文章图片
原理就这样,一般使用场景:绘制完成回调,例子可参见:
《你知道 android 的 MessageQueue.IdleHandler 吗?》
也可以在一些开源项目上看到IdleHandler的应用:
useof.org/java-open-s…
0x4、一些其他问题 1.Looper在主线程中死循环,为啥不会ANR?
答:上面说了,Looper通过queue.next()获取消息队列消息,当队列为空,会堵塞,你可能会问:主线程都堵住了,怎么响应用户操作和回调Activity声明周期相关的方法?
此时主线程也堵塞在这里,好处是:main函数无法退出,APP不会一启动就结束!
答:application启动时,可不止一个main线程,还有其他两个Binder线程:ApplicationThread 和 ActivityManagerProxy,用来和系统进程进行通信操作,接收系统进程发送的通知。
文章图片
死循环不会ANR,但是 dispatchMessage 中又可能会ANR哦!如果你在此执行一些耗时操作,
- 当系统受到因用户操作产生的通知时,会通过 Binder 方式跨进程通知 ApplicationThread;
- 它通过Handler机制,往 ActivityThread 的 MessageQueue 中插入消息,唤醒了主线程;
- queue.next() 能拿到消息了,然后 dispatchMessage 完成事件分发;
PS:ActivityThread 中的内部类H中有具体实现
导致这个消息一直没处理完,后面又接收到了很多消息,堆积太多,就会引起ANR异常!!!
2.Handler泄露的原因及正确写法 上面说了,如果直接在Activity中初始化一个Handler对象,会报如下错误:
文章图片
原因是:
在Java中,非静态内部类会持有一个外部类的隐式引用,可能会造成外部类无法被GC;而单单使用静态内部类,Handler就不能调用Activity里的非静态方法了,所以加上「弱引用」持有外部Activity。
比如这里的Handler,就是非静态内部类,它会持有Activity的引用从而导致Activity无法正常释放。
代码示例如下:
private static class MyHandler extends Handler {
//创建一个弱引用持有外部类的对象
private final WeakReference content;
private MyHandler(MainActivity content) {
this.content = new WeakReference(content);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity= content.get();
if (activity != null) {
switch (msg.what) {
case 0: {
activity.notifyUI();
}
}
}
}
}
转换成Kotlin:(Tips:Kotlin 中的内部类,默认是静态内部类,使用inner修饰才为非静态~)
private class MyHandler(content: MainActivity) : Handler() {
//创建一个弱引用持有外部类的对象
private val content: WeakReference = WeakReference(content)override fun handleMessage(msg: Message) {
super.handleMessage(msg)
val activity = content.get()
if (activity != null) {
when (msg.what) {
0 -> {
activity.notifyUI()
}
}
}
}
}
3.同步屏障机制 通过上面的学习,我们知道用Handler发送的Message后,MessageQueue的enqueueMessage()按照 时间戳升序 将消息插入到队列中,而Looper则按照顺序,每次取出一枚Message进行分发,一个处理完到下一个。这时候,问题来了:有一个紧急的Message需要优先处理怎么破?你可能或说直接sendMessage()不就可以了,不用等待立马执行,看上去说得过去,不过可能有这样一个情况:
一个Message分发给Handler后,执行了耗时操作,后面一堆本该到点执行的Message在那里等着,这个时候你sendMessage(),还是得排在这堆Message后,等他们执行完,再到你!对吧?Handler中加入了「同步屏障」这种机制,来实现「异步消息优先执行」的功能。
添加一个异步消息的方法很简单:
一般情况下:同步消息和异步消息没太大差别,但仅限于开启同步屏障之前。可以通过 MessageQueue 的 postSyncBarrier 函数来开启同步屏障:
- 1、Handler构造方法中传入async参数,设置为true,使用此Handler添加的Message都是异步的;
- 2、创建Message对象时,直接调用setAsynchronous(true)
文章图片
行吧,这一步简单的说就是:往消息队列合适的位置插入了同步屏障类型的Message (target属性为null)
接着,在 MessageQueue 执行到 next() 函数时:
文章图片
遇到target为null的Message,说明是同步屏障,循环遍历找出一条异步消息,然后处理。
在同步屏障没移除前,只会处理异步消息,处理完所有的异步消息后,就会处于堵塞。
如果想恢复处理同步消息,需要调用 removeSyncBarrier() 移除同步屏障:
文章图片
在API 28的版本中,postSyncBarrier()已被标注hide,但依旧可在系统源码中找到相关应用,比如:
为了更快地响应UI刷新事件,在ViewRootImpl的scheduleTraversals函数中就用到了同步屏障:
文章图片
4.Android 11 R Handler 变更 官方文档:developer.android.google.cn/reference/a…
构造函数:最后 只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是浮于表象,这对我们的知识体系的建立和完备以及实战技术的提升都是不利的。
Looper.prepareMainLooper () 废弃
- Handler() 废弃 → Handler(Looper.myLooper())
- Handler(Handler.Callback callback) 废弃 → Handler(Looper.myLooper(), callback)
原因:主线程的Looper是由系统自动创建的,无需用户自行调用。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读Android系统源码,还包括各种优秀的开源库。
最后为了帮助大家深刻理解Handler相关知识点的原理以及面试相关知识,这里还为大家整理了Android开发相关源码精编解析:
深入解析 Handler 源码解析
- 发送消息
- 消息入队
- 消息循环
- 消息遍历
- 消息的处理
- 同步屏障机制
- 阻塞唤醒机制
文章图片
文章图片
还有Handler相关面试题解析帮助熟练掌握Handler知识:
文章图片
文章图片
最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上我搜集整理的2019-2020BAT 面试真题解析,我把大厂面试中常被问到的技术点整理成了PDF,包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
《960全网最全Android开发笔记》
文章图片
《379页Android开发面试宝典》
历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。
如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数
文章图片
《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
文章图片
【换个姿势,带着问题深入学习Handler,手撕吊打面试官!】资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图,以上资源均免费分享,以上内容均放在了开源项目:【github】 中已收录,大家可以自行获取(或者关注主页扫描加微信获取)。
推荐阅读
- 闺蜜,太甜蜜的词会带着刺
- [个案学习070]|[个案学习070] 一个带着童年创伤的个案如何推进
- 学会换个角度看问题
- 【SpringBoot】Intellij开发微服务的正确姿势
- 这是个带着有色眼镜的社会
- 期待你的成长
- 关于启动页全屏的正确姿势
- 梦和远方
- 手写我心day12
- “底薪+提成”已经过时了,换个薪酬模式,留住优秀员工,干货