写点什么

换个姿势,带着问题深入学习 Handler,手撕吊打面试官,kotlin 单例 firbase

用户头像
Android架构
关注
发布于: 35 分钟前
  • 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 = 201000; // 前台 static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10; // 后台// ActivityManagerService.java → Broadcast 广播、InputDispatching、ContentProviderstatic final int BROADCAST_FG_TIMEOUT = 101000; // 前台 static final int BROADCAST_BG_TIMEOUT = 601000; // 后台 static final int KEY_DISPATCHING_TIMEOUT = 51000; // 关键调度 static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10*1000; // 内容提供者


时间统计区间:


  • 起点System_Server?进程调用?startProcessLocked?后调用?AMS.attachApplicationLocked()


  • 终点Provider?进程?installProvider publishContentProviders?调用到?AMS.publishContentProviders()


  • 超过这个时间,系统就会杀掉?Provider?进程。


二、Handler 怎么用



1.sendMessage() + handleMessage()

代码示例如下



黄色部分会有如下警告


![](https://img-blog.csdnimg.cn/img_convert/834729


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


117c629e85282ce10f338dea63.png)


Handler 不是静态类可能引起「内存泄露」,原因以及正确写法等下再讲。


另外,建议调用?Message.obtain()?函数来获取一个 Message 实例,为啥?点进源码:



从源码,可以看到 obtain()的逻辑:


  • 加锁,判断 Message 池是否为空


  • ① 不为空,取一枚 Message 对象,正在使用标记置为 0,池容量-1,返回;


  • ② 为空,新建一个 Message 对象,返回;


Message 池复用 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,


value 是 set 的那个值。get 的时候,都是从自己的变量中取值,所以不存在线程安全问题。


主线程和子线程的 Looper 对象实例相互隔离的!!!


意味着:主线程和子线程 Looper 不是同一个!!!


知道这个以后,有个问题就解惑了:


为什么子线程中不能直接 new Handler(),而主线程可以?


答:主线程与子线程不共享同一个 Looper 实例,主线程的 Looper 在启动时就通过


prepareMainLooper() 完成了初始化,而子线程还需要调用?Looper.prepare()


和?Looper.loop()开启轮询,否则会报错,不信,可以试试:



直接就奔溃了~



加上试试?



可以,程序正常运行,没有报错。


对了,既然说 Handler 用于子线程和主线程通信,试试在主线程中给子线程的 Handler 发送信息,修改一波代码:



运行,直接报错:



原因:多线程并发的问题,当主线程执行到 sendEnptyMessage 时,子线程的 Handler 还没有创建


一个简单的解决方法是:主线程延时给子线程发消息,修改后的代码示例如下:



运行结果如下:



可以,不过其实 Android 已经给我们封装好了一个轻量级的异步类「HandlerThread

4.HandlerThread

HandlerThread?=?继承 Thread?+?封装 Looper


使用方法很简单,改造下我们上面的代码:



用法挺简单的,源码其实也很简单,跟一跟:





剩下一个 quit()和 quitSafely()停止线程,就不用说了,所以 HandlerThread 的核心原理就是:


  • 继承 Thread,getLooper()加锁死循环 wait()堵塞线程;


  • run()加锁等待 Looper 对象创建成功,notifyAll()唤醒线程


  • 唤醒后,getLooper 返回由 run()中生成的 Looper 对象


是吧,HandlerThread 的实现原理竟简单如斯,另外,顺带提个醒!!!


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{Thread.currentThread()}")


lock.notifyAll()}fun consume() = synchronized(lock) {


while(items<=0) {


lock.wait()


}


Thread.sleep(rand.nextInt(100).toLong())


items--


println("Consumed, count is{Thread.currentThread()}")


lock.notifyAll()


}

5.当我们用 Handler 发送一个消息发生了什么?

扯得有点远了,拉回来,刚讲到?ActivityThread?在?main 函数中调用?Looper.prepareMainLooper


完成主线程?Looper 初始化,然后调用?Looper.loop()?开启消息循环?等待接收消息


嗯,接着说下?发送消息,上面说了,Handler 可以通过 sendMessage()和?post() 发送消息,


上面也说了,源码中,这两个最后调用的其实都是?sendMessageDelayed()完成的:



第二个参数:当前系统时间+延时时间,这个会影响「调度顺序」,跟?sendMessageAtTime()



获取当前线程 Looper 中的 MessageQueue 队列,判空,空打印异常,否则返回?enqueueMessage(),跟:

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
换个姿势,带着问题深入学习Handler,手撕吊打面试官,kotlin单例firbase