写点什么

换个姿势,带着问题看 Handler

用户头像
Android架构
关注
发布于: 5 小时前


打脸?莫慌,在子线程代码块中加一句休眠模拟耗时操作:



程序就崩溃了,报错日志如下:



前面说了 Android 的 UI 更新被设计成单线程,但为何在添加休眠后才报错,限于篇幅,不跟源码,直接说原因:


  • ViewRootImponCreate() 调用时还没创建;

  • onResume() 时,即 **ActivityThread.handleResumeActivity()**执行后才创建;

  • 调用 View.requestLayout(),最终调到 ViewRootImpl.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 = 101000; // 前台 static final int BROADCAST_BG_TIMEOUT = 601000; // 后台 static final int KEY_DISPATCHING_TIMEOUT = 51000; // 关键调度 static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 101000; // 内容提供者


时间统计区间:


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

  • 终点Provider 进程的 **installProvider()**及 publishContentProviders() 调用到 AMS.publishContentProviders()


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

0x2、Handler 怎么用

1.sendMessage() + handleMessage()

代码示例如下



黄色部分会有如下警告



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


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



从源码,可以看到 obtain()的逻辑:加锁判断 Message 池是否为空


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

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


此处复用 Message,可避免**避免重复创建实例对象,达到节约内存的目的,而且不难看出 Message 实际上是无头结点的单链表**。



上述获取消息池的逻辑:



定位到下述代码,还可以知道:池的容量为 50



然后问题来了,Message 信息什么时候加到池中?


答:当 Message 被 Looper 分发完后,会调用 recycleUnchecked()函数,回收没有在使用的 Message 对象。



标志设置为**FLAG_IN_USE**,表示正在使用,相关属性重置,加锁,判断消息池是否满,未满,单链表头插法 将消息插入到表头。

2.post(runnable)

代码示例如下


![ ](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0ff


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


2305b372140a6ad86d830b06396b6~tplv-k3u1fbpfcp-zoom-1.image)


跟下 post():



实际上调用了 sendMessageDelayed() 发送消息,只不过延迟秒数为 0,那 Runnable 是怎么变成 Message 的呢?跟下 getPostMessage()



噢,获取一个新的 Message 示例后,把 Runnable 变量的值赋值给 callback属性


3.附:其他两个种在子线程中更新 UI 的方法

activity.runOnUiThread()



view.post() 与 view.postDelay()


0x3、Handler 底层原理解析

终于来到稍微有点技术含量的环节,在观摩源码了解原理前,先说下几个涉及到的类。

1.涉及到的几个类

2.前戏

在我们使用 Handler 前,Android 系统已为我们做了一系列的工作,其中就包括了


创建「Looper」和「MessageQueue」对象


上图中有写:ActivityThreadmain 函数是 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 对象实例相互隔离的!!!


另外,线程为 key 也保证了每个线程只有一个 Looper,而创建 Looper 对象时又会创建 MessageQueue 对象,所以间接保证每个线程最多只能有一个 MessageQueue。


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


为什么子线程中不能直接 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 发送一个消息发生了什么?

扯得有点远了,拉回来,刚讲到 ActivityThreadmain 函数中调用 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 第一次处理消息,有一个消息处理完 ; 大于 0 时,最长堵塞等待时间,期间有新消息进来,可能会了立即返回(立即执行); 等于-1 时,无消息时,会一直堵塞;


此处没有用 java 中的 wait/notify 堵塞,而是通过 Linux 的**epoll机制**来堵塞,原因是需要处理 native侧 的事件。


没有消息时堵塞并进入休眠释放 CPU 资源,有消息时再唤醒线程。


对 epoll 机制感兴趣的可移步至下述网站查阅:


Linux IO模式及 select、poll、epoll详解

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()**获取消息队列消息,当队列为空,会堵塞,


此时主线程也堵塞在这里,好处是:main 函数无法退出,APP 不会一启动就结束!


你可能会问:主线程都堵住了,怎么响应用户操作和回调 Activity 生命周期相关的方法?


答:application 启动时,可不止一个 main 线程,还有其他两个 Binder 线程ApplicationThreadActivityManagerProxy,用来和系统进程进行通信操作,接收系统进程发送的通知。



  • 当系统受到因用户操作产生的通知时,会通过 Binder 方式跨进程通知 ApplicationThread;

  • 它通过 Handler 机制,往 ActivityThreadMessageQueue 中插入消息,唤醒了主线程;

  • queue.next() 能拿到消息了,然后 dispatchMessage 完成事件分发;


Tips:ActivityThread 中的内部类 H 中有具体实现


死循环不会 ANR,但是 dispatchMessage 中又可能会 ANR 哦!如果你在此执行一些耗时操作,导致这个消息一直没处理完,后面又接收到了很多消息,堆积太多,就会引起 ANR 异常!!!

2.Handler 泄露的原因及正确写法

上面说了,如果直接在 Activity 中初始化一个 Handler 对象,会报如下错误:



原因是


在 Java 中,非静态内部类会持有一个外部类的隐式引用,可能会造成外部类无法被 GC; 比如这里的 Handler,就是非静态内部类,它会持有 Activity 的引用从而导致 Activity 无法正常释放。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
换个姿势,带着问题看Handler