Android---彻底掌握 Handler

Handler 现在几乎是 Android 面试的必问知识点,大多数 Adnroid 工程师都在项目中使用过 Handler。主要场景是子线程完成耗时操作的过程中,通过 Handler 向主线程发送消息 Message,用来刷新 UI 界面。

下面我们来了解 Handler 的发送消息和处理消息的源码实现。分析源码的时候最好找到一个合适的切入点,Handler 源码的一个切入点就是它的默认构造器。

从 new Handler 开始

在无参的构造函数里调用重载的方法,并分别传入 null 和 false。并且在构造方法中给两个全局变量赋值,mLooper 和 mQueue。

mLooper 和 mQueue 都是通过 Looper 来获取,具体代码如下

可以看出,mQueue 是 Looper 中的一个全局变量,类型是 MessageQueue 类

这个 Looper 是什么?何时被初始化?

Looper 介绍

不知大家在开发的时候有没有思考过这么一个问题呢?启动一个 Java 程序的入口函数是 main 方法,当 main 方法执行完毕之后此程序停止运行,也就是进程会自动终止。但是,当打开一个 Activity 之后,只要不按下返回键 Activity 会一直显示在屏幕上。也就是 Activity 所在进程会一直处于运行状态。实际上,Looper 内部维护一个无限循环,保证 App 进程持续进行

Looper 初始化

ActivityThread.java 的 mian 方法是一个新 app 进程的入口,其具体实现如下

图中1处就是初始化当前进程的 Looper 对象;图中2处调用 Looper 的 loop() 方法开启无限循环。

prepareMainLooper() 方法如下

图中1处,在 prepareMainLooper() 方法中调用 prepare() 方法创建 Looper 对象,将 new 出的 Looper 设置到线程本地变量 sThreadLocal 中,也就是说创建的 Looper 与当前线程发生了绑定。

Looper 的构造方法如下

可以看出在构造方法中,初始化了消息队列 MessageQueue 对象。

Looper 方法执行完之后会在图中3处执行 myLooper() 方法从 sThreadLocal 中取出 Looper 对象并复制给 sMainLooper 对象。图中2处在创建 Looper 对象之前会判断 sThreadLocal 中是否已经绑定过 Looper 对象,如果是则抛出异常。这行代码的目的是确保在一个线程中 Looper.prepare() 方法只能被调用1次。比如执行如下代码,程序秒崩

打印日志如下

不是说调用2次 prepare 才会抛异常吗?为什么 MainActivity 中只调用了1遍就导致程序崩溃?

这是因为,在 MainActivity 所在进程被创建时,Looper 的 prepare 方法已经在 main 方法中调用了1遍,这会导致一个非常重要的结果:

\bullet prepare 方法在一个线程中只能被调用1次;

\bullet Looper 的构造方法在一个线程中只能被调用1次;

\bullet 最终导致 MessageQueue 在一个线程中只会被初始化1次

也就是说,UI 线程中只会存在一个 MessageQueue 对象。后续我们通过 Handler 发送的消息都会发送到这个 MessageQueue 当中。

Looper 负责做什么事情

用一句话总结就是,Looper 做的事情是不断从 MessageQueue 中取出 Message,然后处理 Message 中指定的任务

在 ActivityThread 的 main 方法中,除了调用 Looper.prepareMainLooper() 初始化 Looper 对象之外,还调用了 Looper.loop() 方法开启了无限循环。Looper 的主要功能就是在这个循环中完成的。

很显然,loop() 方法执行了一个死循环,这也是一个 Android app 进程能够持续运行的原因。图中1处不断调用 MessageQueue 的 next 方法取出 message。如果 message 不为 null,则调用图中2处进行后续处理。具体就是从 message 中取出 target 对象,然后调用其 dispatchMessage() 方法处理 message 自身。那么这个 target 是谁呢?

查看 Message.java 源码,可以看出 target 就是 Handler 对象,如下所示

Handler 的 dispatchMessage() 方法

可以看出,在 dispatchMessage 方法中调用一个空方法 handleMessage(),而这个方法正是我们创建 Handler 时需要复写的方法那么 Handler 是何时将其设置为一个 message 的 target 呢?(在 Handler 的 enqueueMessage 方法中)

Handler 的 sendMessage() 方法

Handler 有几个重载的 sendMessage() 方法,但是基本都大同小异,代码具体如下

经过几层调用之后,sendMessage 最终会调用 enqueueMessage() 方法,将 message 插入到消息队列 MessageQueue 当中。这个消息队列就是我们上面分析的,在 ActivityThread 的 main 方法中通过 Looper 创建的 MessageQueue。

Handler 的 enqueueMessage 方法

可以看出,在图中1处 enqueueMessage 方法中将 Handler 自身设置为 Message 的 target 对象。因此,后续 message 会调用此 Handler 的 dispatchMessage 来处理。图中2处会判断,如果 message 中的 target 没有被设置,则直接抛出异常。图中3处会按照 Message 的时间(when)来有序的插入 MessageQueue 中。可以看出 MessageQueue 实际上是一个有序队列,只不过是按照 message 的执行时间来排序的。

至此,Handler 的发送消息和消息处理流程已经介绍完毕。

有关 Handler 的面试题

1. Handler 的 post(Runnable) 与 sendMessage() 有什么区别?

看一下 post(Runnable) 的实现源码,如下

实际上 post(Runnable) 会将 Runnable 复制到 message 的 callback 变量中。那么这个 Runnable 是在什么地方被执行的呢?Looper 从 MessageQueue 中取出 Message 之后,会调用 dispatchMessage 方法进行处理。再看一下其实现

可以看出 dispatchMessage 分两种情况,如果 message 的 callback 不为null,一般为通过 Post(Runnable) 方式,会直接执行 Runnable 的 run 方法。因此这里的 Runnable 实际上就是一个回调接口,和线程 Thread 没有任何关系;如果 message 的 callback 为 null,这种一般为 sendMessage 方式,用 Handler 的 handleMessage() 方法进行处理。

2. Looper.loop() 为什么不会阻塞主线程

上面我们提到,Looper 中的 loop 方法实际上是一个死循环,但是我们的 UI 线程却并没有被阻塞,反而还能够进行各种手势操作,这是为什么呢?

在 MessageQueue 的 next 方法中,有如下一段代码

nativePollOnce 方法是一个 native 方法,当调用此 native 方法时,主线程会释放 CPU 资源进入休眠状态,直到下一条消息到达或者有事务发生,通过往 pipe 管道写端写入数据来唤醒主线程工作。

3. Handler 的 sendMessageDelayed 或者 postDelayed 是如何实现的

在向 MessageQueue 队列中插入 Message 时,会根据 Message 的执行时间排序,而消息的延时处理的核心实现是在获取 Message 的阶段。接下来看一下 MessageQueue 的 next 方法

图中蓝框处表示从 MessageQueue 中取出一个 Message,但是当前的系统时间小于 message.when,因此会计算一个 TimeOut 。目的是为了实现在 TimeOut 后再将 UI 线程唤醒。因此后续处理 TimeOut 的代码,只会在 TimeOut 时间后才会被 CPU 执行。

注意,在上述代码中也能看出。如果当前系统时间大于或等于 Message.when,那么会返回 Message 给 Looper.loop(),但是这个逻辑只能保证在 when 之前消息不被处理,不能保证一定在 when 时被处理。

总结

\bullet 应用启动是从 ActivityThread 的 main 开始的。先是执行了 Looper.prepare(),该方法先是 nwe 了一个 Looper 对象,又在私有的构造方法中创建了 MessageQueue 作为此 Looper 对象的成员变量。Looper 对象通过 ThreadLocal 绑定在 MainThread 中。

\bullet 当创建 Handler 子类对象时,在构造方法中通过 ThreadLocal 获取绑定的 Looper 对象,并获取此 Looper 对象的成员变量 MessageQueue 作为该 Handler 对象的成员变量。

\bullet 在子线程中调用上一步创建的 Handler 子类对象的 sendMessage(msg)方法时,在该方法中将 msg 的 target 属性设置为自己本身,同时调用成员变量 MessageQueue 对象的 enqueueMessage() 方法将 msg 放入 MessageQueue 中。

\bullet 主线程创建好之后,会执行 Looper.loop() 方法,该方法获取与线程绑定的 Looper 对象,继而获取该 Looper 对象的成员变量 MessageQueue 对象。并开启一个会阻塞(不占用资源)的死循环,只要 MessageQueue 中有 msg,就会获取该 msg 并执行 msg.target.dispatchMessage(msg) 方法(msg.target 即上一步引用的 handler 对象),此方法中调用了第二步创建 handler 子类对象时覆写的 handleMessage() 方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/132660.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

吃透BGP,永远绕不开这些基础概述,看完再也不怕BGP了!

你们好,我的网工朋友。 总有人在私信里抱怨,BGP实在是太难了! 一是这玩意儿本来就很复杂,需要处理大量的路由信息和复杂的算法;再一个是需要你有一定的实战经验才能深入理解运作。 虽然BGP确实有一定难度&#xff0c…

【漏洞复现】Metinfo6.0.0任意文件读取漏洞复现

感谢互联网提供分享知识与智慧,在法治的社会里,请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现代码审计漏洞点 1.5、深度利用EXP编写 1.6、漏洞挖掘1.7修复建议 1.1、漏洞描述 漏洞名称:MetInfo任意文件…

Node.js |(五)包管理工具 | 尚硅谷2023版Node.js零基础视频教程

学习视频:尚硅谷2023版Node.js零基础视频教程,nodejs新手到高手 文章目录 📚概念介绍📚npm🐇安装npm🐇基本使用🐇生产依赖与开发依赖🐇npm全局安装🐇npm安装指定包和删除…

【Spring Boot 源码学习】JedisConnectionConfiguration 详解

Spring Boot 源码学习系列 JedisConnectionConfiguration 详解 引言往期内容主要内容1. RedisConnectionFactory1.1 单机连接1.2 集群连接1.3 哨兵连接 2. JedisConnectionConfiguration2.1 RedisConnectionConfiguration2.2 导入自动配置2.3 相关注解介绍2.4 redisConnectionF…

链表面试OJ题(1)

今天讲解两道链表OJ题目。 1.链表的中间节点 给你单链表的头结点 head ,请你找出并返回链表的中间结点。 如果有两个中间结点,则返回第二个中间结点。 示例 输入:head [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个…

Chromebook文件夹应用新功能

种种迹象表明 Google 旗下的 Chromebooks 近期要有大动作了。根据 Google 团队成员透露,公司计划在 Chrome OS 的资源管理器中新增“Recents”(最近使用)文件,以便于用户更快找到所需要的文件。 种种迹象表明 Google 旗下的 Chro…

【数据结构】排序算法复杂度 及 稳定性分析 【图文详解】

排序算法总结 前言[ 一 ] 小数据基本排序算法(1)冒泡排序(2)直接插入排序 [ 二 ] (由基本排序衍生的用作)处理大数据处理排序(1)堆排序(2)希尔排序 [ 三 ] 大…

unity中移动方案--物理渲染分层

一、三种基本移动方案 unity中的移动分为Transform和Rigidbody以及CharacterController,其中CharacterController功能完善,已经可以避免了穿墙,并实现了贴墙走等情况,需要结合性能考虑选择不同的方式。 1.使用transform,直接修改…

鳄鱼指标的3颜色线都代表什么?澳福官网一段话明白了

投资者一直在使用鳄鱼指标进行交易,但是对指标上面的3种颜色的K线都代表什么不明白?直到看到澳福官网一段话才明白,原来这么简单! 鳄鱼指标,这一工具是由三条移动平均线组合而成。具体来说,蓝线&#xff0…

Spring Boot 3系列之-启动类详解

Spring Boot是一个功能强大、灵活且易于使用的框架,它极大地简化了Spring应用程序的开发和部署流程,使得开发人员能够更专注于业务逻辑的实现。在我们的Spring Boot 3系列之一(初始化项目)文章中,我们使用了Spring官方…

桶装水订水系统水厂送水小程序开发;

桶装水小程序正式上线,支持多种商品展示形式,会员卡、积分、分销等功能; 开发订水送水小程序系统,基于用户、员工、商品、订单、配送站和售后管理模块,对每个模块进行统计分析,简化了分配过程,提…

【教3妹学编程-算法题】最大单词长度乘积

3妹:哇,今天好冷啊, 不想上班。 2哥:今天气温比昨天低8度,3妹要空厚一点啊。 3妹 : 嗯, 赶紧把我的羽绒服找出来穿上! 2哥:哈哈,那倒还不至于, 不过气温骤降&…

【简朴PlainApp】通过浏览器就能管理手机文件的开源利器

简朴PlainApp 简朴是一个开源应用,允许您通过网络浏览器管理您的手机。您可以通过安全、易于使用的网络界面从桌面访问文件、视频、音乐、联系人、短信、通话记录等等! 这对于手机上文件较多的朋友来说,从任意电脑上管理手机文件的需求还是挺…

详解IPD需求分析工具$APPEALS

够让企业生存下去的是客户,所以,众多企业提出要“以客户为中心”,那如何做到以客户为中心?IPD中给出的答案是需求管理。 需求管理流程,是IPD(集成管理开发)体系中的四大支撑流程之一&#xff0…

【源码解析】Spring Bean定义常见错误

案例1 隐式扫描不到Bean的定义 RestController public class HelloWorldController {RequestMapping(path "/hiii",method RequestMethod.GET)public String hi() {return "hi hellowrd";}}SpringBootApplication RestController public class Applicati…

基于AOSP源码Android-10.0.0_r41分支编译,framework开发,修改系统默认字体大小

文章目录 基于AOSP源码Android-10.0.0_r41分支编译,framework开发,修改系统默认字体大小 基于AOSP源码Android-10.0.0_r41分支编译,framework开发,修改系统默认字体大小 主要修改一个地方就行 代码源码路径 frameworks/base/co…

python df.apply()函数

DataFrame.apply(func, axis0, rawFalse, result_typeNone, args(), by_row‘compat’, kwargs) 沿df的轴应用函数 func:对每行或者每列应用的函数axis{0 or ‘index’, 1 or ‘columns’}, default 0 0是列,1是行 rawbool, default False result_type**{‘expand…

单通道低压 H 桥电机驱动芯片AT9110H 兼容L9110 马达驱动芯片

H桥直流电机驱动电路是一种用于控制直流电机运转的电路,其主要特点是可以实现正反转控制,控制电机转速和方向,同时也具有过流保护功能。 H桥电路由四个功率晶体管和一些辅助电路组成,其中两个晶体管用于控制电机正转,…

【Linux】Nignx及负载均衡动静分离

🎉🎉欢迎来到我的CSDN主页!🎉🎉 🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚 🌟推荐给大家我的专栏《微信小程序开发实战》。🎯&#x1f3a…

BGF-YOLO | 增强版YOLOV8 | 用于脑瘤检测的多尺度注意力特征融合

基于You Only Look Once(YOLO)的目标检测器在自动脑瘤检测中展现出卓越的准确性。在本文中,我们开发了一种新的BGF-YOLO架构,通过将双层路由注意力(BRA)、广义特征金字塔网络(GFPN)和第四检测头整合到YOLOv8中来实现。BGF-YOLO包含了一个注意力机制,用于更加关注重要的…