Android---彻底掌握 Handler

news2025/1/15 22:32:10

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.coloradmin.cn/o/1174652.html

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

相关文章

Jupyter 两个炸裂的骚操作!

大家好,Jupyter的常用功能不多说了,关注我的粉丝相信都比较熟悉了,不了解的可以看看历史文章。 今天聊两个不太常见但很有用的骚操作,可以为我们节省大量的时间,提高效率。 下面我们开始介绍。 一、获取历史执行信息…

思维模型 权威效应

本系列文章 主要是 分享 思维模型,涉及各个领域,重在提升认知。人微言轻、人贵言重 1 权威效应的应用 1.1 苹果公司的权威效应和品牌效应 苹果公司是全球知名的科技公司,其产品以高品质、高性能和高设计感著称。苹果公司的品牌效应和权威效…

Java8实战-总结46

Java8实战-总结46 CompletableFuture:组合式异步编程让代码免受阻塞之苦使用 CompletableFuture 发起异步请求寻找更好的方案 CompletableFuture:组合式异步编程 让代码免受阻塞之苦 使用 CompletableFuture 发起异步请求 可以使用工厂方法supplyAsyn…

复杂物体线结构光中心线提取方法研究

论文地址:Excellent-Paper-For-Daily-Reading/application/centerline at main 类别:应用——中心线提取 时间:2023/11/05 摘要 针对复杂物体动态三维测量中条纹图像过曝光、欠曝光以及环境光照干扰引起激光中心线提取速度慢、提取 不准确…

vue3项目实践

创建 vue3 项目 node本版:node 16.x.x, 脚手架:create-vue 脚手架工具,底层vite 创建vue3项目:npm init vuelatest setup函数 vue3 单文件组件 1、vite.config.js配置文件基于vite的配置 2、template模板不再要求唯…

从源码中看@Qualifier注解

theme: smartblue 摘要 Qualifier注解通常和Autowired注解一起使用,那么首先来看Autowire是怎么把Bean对象注入到Spring容器中的。 前置-Autowired注入原理 前置条件:需要读者了解Autowired是如何将类注入进来的。 深入解析 Spring Framework 中 Au…

【QT基础入门 控件篇】QLineEdit 基础、高级和样式表使用详解

一、QLineEdit简介 QLineEdit是一个单行文本编辑器,它可以让用户输入和编辑纯文本,也可以设置一些有用的编辑功能,如撤销和重做、剪切和粘贴、拖放等。QLineEdit: 可以根据不同的回显模式(echoMode)来显示不同的输入内…

pg14-sql基础(三)-分组

分组 SELECT hire_date, COUNT(*) FROM employees GROUP BY hire_date;SELECT extract(year from hire_date), COUNT(*) FROM employees GROUP BY extract(year from hire_date); -- GROUP BY 1;SELECT extract(year from hire_date), department_id, COUNT(*) FROM employees…

springboot 连接西门子plc,读取对应的值,并修改到数据库

springboot 连接西门子plc,读取对应的值,并修改到数据库 需求:服务器连接plc,读取数据,之后写入到数据库,但是要求速度很快,而且plc中命令对应的值是不断变化的,这个变化&#xff0c…

Python基础入门例程46-NP46 菜品的价格(条件语句)

最近的博文: Python基础入门例程45-NP45 禁止重复注册(条件语句)-CSDN博客 Python基础入门例程44-NP44 判断列表是否为空(条件语句)-CSDN博客 Python基础入门例程43-NP43 判断布尔值(条件语句&#xff0…

[原创]Cadence17.4,win64系统,构建CIS库

目录 1、背景介绍 2、具体操作流程 3、遇到问题、分析鉴别问题、解决问题 4、借鉴链接并评论 1、背景介绍 CIS库,绘制原理图很方便,但是需要在Cadence软件与数据库之间建立联系,但是一直不成功,花费半天时间才搞明白如何建立关系并…

思维模型 门槛效应/登门槛效应

本系列文章 主要是 分享 思维模型,涉及各个领域,重在提升认知。跨过一个个门槛,走向你该走向的“深渊”和“光明”。 说明:后面 门槛效应/登门槛效应 均使用门槛效应替代 1 门槛效应的应用 1.1 营销策略中的门槛效应 免费试用&…

二维码智慧门牌管理系统升级:一键报警让你的生活更安全!

文章目录 前言一、升级解决方案的特点二、实施步骤 前言 随着科技的不断进步,我们的生活正在逐渐变得更加智能化。可以想象一下,如果你家的门牌也能拥有这种智能升级,将会带来怎样的改变?今天,让我们一起探讨这令人兴…

【Windows】Google和火狐浏览器禁用更新的操作方式

想必很多网民常用的浏览器是Edge,Google,火狐这三种,但是浏览器都有后台自动更新,更新提示会一直显示,要用户去点击才关掉,有点强迫症的用户就会想要把它一直关掉,可每次打开都关不掉&#xff0…

Qt下使用动画框架实现动画520

文章目录 前言一、动画框架的介绍二、示例完整代码三、QtCreator官方示例总结 前言 文章中引用的内容均来自这本书中的原文:【Qt Creator快速入门_霍亚飞编著】。可以通过更改本文示例pro文件中的条件编译,运行示例1和示例2来查看串行动画组和并行动画组…

Qt应用开发--国产工业开发板T113-i的部署教程

Qt在工业上的使用场景包括工业自动化、嵌入式系统、汽车行业、航空航天、医疗设备、制造业和物联网应用。Qt被用来开发工业设备的用户界面、控制系统、嵌入式应用和其他工业应用,因其跨平台性和丰富的功能而备受青睐。 Qt能够为工业领域带来什么好处: - …

基于SSM的网吧计费管理系统(有报告)。Javaee项目,ssm项目。

演示视频: 基于SSM的网吧计费管理系统(有报告)。Javaee项目,ssm项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结构,通…

Docker容器技术实战4

11、docker安全 proc未被隔离,所以在容器内和宿主机上看到的东西是一样的 容器资源控制 cpu资源限制 top命令,查看cpu使用率 ctrlpq防止退出回收,容器会直接调用cgroup,自动创建容器id的目录 cpu优先级设定 测试时只保留一个cpu…

Spring的总结

SpringFramework 认识SpringFramework 最首先,我们先要认识Spring和SpringFramework两者之间有什么联系? 广义上的 Spring 泛指以 Spring Framework 为基础的 Spring 技术栈。 狭义的 Spring 特指 Spring Framework,通常我们将它称为 Spr…

Linux多值判断利用case...esac判断

利用这个判断,一定要注意格式的运用,非常容易出错 case $1 in #判断变量的值 "hello") #双引号注意,右括号 echo " afdbab " #语句段,没啥说的 ;; #两个分号结束第一个判断&#xff0c…