二十三、Handler 从源码分析到全面掌握

概述

Handler是 安卓面试的必问点。在安卓开发中,handler经常用于子线程执行异步任务,然后通知到主线程更新UI。 本文将从源码分析开始,一步步了解到 handler 核心知识。

从 new Handler() 开始

Handler有一个无参构造函数。如下:

上图中 无参构造函数,实际上调用了 Hanlder(Callback callback,boolean async),而在这个有参构造器中,两个关键点

  • 创建了一个 Looper对象,赋值给了 mLooper
  • 将 mLooper中的.mQueue 赋值 给了 本类中的mQueue

说明 mLooper 和 mQueue 是 handler这个消息处理体系中的重要组成部分。

Looper是什么? 它何时被初始化?

启动一个Java程序人入口是一个 main函数,如果main函数执行完毕,那么,程序就会停止运行,app进程就会终止。但是我们打开一个App之后,除非我们按下返回键或者home键回到桌面,不然它就是一直处于运行状态。s

也就是说:Looper维护了一个无限循环,保证app进程一直在运行,这是一个重要概念,必须理解,并且记清楚。

在安卓的app中,启动app的入口函数实际上是 ActivityThread.java的main函数。 如下图: s

图中1处,创建出当前线程的Looper对象;

图中2处,启动这个looper对象的无限循环。

可是说起来是 创建了 当前线程的Looper对象,但是貌似并没有在创建时指定线程对象啊? 请看下图:

1处的 prepare(false)的实际上执行到了 prepare(boolean quitAllowed) , 创建出了一个不允许退出的Looper对象,并且将该looper设置到了 sThreaLocal 中。 这使得创建出来的Looper与当前线程发生了绑定。 并且注意,图中2处,是为了保证每一个线程的 Looper只会被创建一次,当有第二次来时,就抛出RuntimeException异常。

Lopper的构造方法如下:

其中创建了一个MessageQueue消息队列,也指定了 Looper的所属线程为 currentThread。

然后,myLooper方法,其实也就是从 sThreadLocal中取出了当前线程的Looper对象。

Looper多次初始化会导致程序崩溃

上图中的Looper.prepare();会导致程序崩溃

,这是因为:该代码运行在主线程中,而主线程的Looper在ActivityThread.java的 main函数中已经 执行了一次。

这也就意味着:

  • Lopper的prepare方法只能在一个线程中执行一次
  • Looper的构造函数只能在一个线程中执行一次
  • 一个线程中的 MessageQueue 只能被初始化一次

Lopper的职责

它的职责很简单,就是 不停地从 MessageQueue中取出 Message 并且执行 Message 指定的任务。

上图的main函数中,有一个 Looper.loop() 开启了无限循环,以保证app进程持续运行。(必须是死循环)

图中1处,从 queue中取出下一个message,

图中2处,取得message.target,并且执行 dispatchMessage.

那么这个target是什么呢?从Message的源码可以看到:

它其实就是Handler

,而 Handler的 中有一个空方法 handlerMessage(),这也就是 我们在创建handler需要重写的方法。

那么Handler是何时成为 一个Message的target的呢?

看下图:

handler的一个重要方法就是发送消息的 sendMessage。 它有一些名称类似的方法,但是作用都是大同小异。基本都走到了 enqueueMessage, 看下图:

图中1处,当发送一个 message时,顺便将 自身this 赋值给了msg.target

图中2处,如果一个message没有设置 target,那么直接抛出异常。

图中3处,按照 messagewhen (执行延时) 来决定插入的位置。

可以看出,MessageQueue其实是一个按照时间顺序来排列的有序队列(数据结构为单链表)。

常见面试题

Handler的 post(Runnable)和sendMessage() 的区别

从 post的源代码分析开始:

将 runnable对象指定为了 message的callback

而在 处理message时,会优先执行 message的callback回调,也就是执行了 Runnable的run方法。

SendMessage时,则没有指定 message的callback。处理 message时,优先执行 Handler的mCallback对象,重写的handlerMessage次之。

所以。一句话,根本上的区别是post(Runnable)和sendMessage()message的处理方式设置的位置不一样。前者就是 runnable本身,后者是 handlerMessage函数,或者是 mCallback的handlerMessage。

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

首先这种提问方式其实是一个陷阱,app主线程能够持续运行的前提,就是 Looper的loop函数中的无限循环能够持续运行。

其次,如果 该应用没有 Message需要处理(也就是暂时没有任何message进入时),app会如何暂时释放CPU资源,而不是持续占用CPU。

此图中1处,queue.next() 后面有一句官方的 注释might block,虚拟语气,说明可能会阻塞。

其实看看内部源码:

这里存在一个 nativePollOnce方法。它是一个native方法。当执行时,主线程会暂时释放CPU,并进入到休眠状态,直到下一条消息到达,或者有事务发生,通过往 pipe管道写端写入数据来唤醒主线程工作。

这里采用的是 c++层的epoll机制。

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

Handler的两个核心逻辑,一个是 线程间数据共享,一个是 发送以及执行延时任务。

上述两种发送消息的方式,都可以通过设置延时时间来 达成业务目的。

具体做法:

MessagQueue 中插入消息时,会根据Message的执行时间进行排序。 而在 处理Message时,核心逻辑在 MessageQueuenext方法中。

蓝色框框中,进行了当前时间消息执行时刻的对比。

  • 如果当前时间已经超出了 消息的执行时刻,那么会马上返回这个message对象给到 Looper去处理。
  • 如果当前时间小于 消息的执行时刻,那么就会计算出一个 时间差,这个时间差就是CPU需要休眠的时间,而由于这是一个for死循环内的过程,所以 nativePoolOnce会马上执行休眠,休眠完成之后再次对比当前时间与 消息的执行时刻。

其实仔细想想,这种方式只能保证 message在when之前不被执行,而不能保证一定在when当前时刻执行。

总结

  • 应用的启动是从 ActivityThread 的 main函数开始,先是执行了 Looper.prepare(),该方法先是 创建了一个Lopper对象,然后在私有方法中又创建了一个 MessageQueue作为Looper的成员变量,Looper对象通过ThreadLocal绑定在了主线程上。

  • 当创建Handler对象时,构造方法中通过ThreadLocal获取绑定的Looper对象,并获取它的MessageQueue作为Handler的成员变量

  • 在子线程中 使用 上一步创建的 Handler子类对象的 sendMessage方法时,将 message的target设置为 自身。同时调用了成员变量的MessageQueue 的 enqueueMessage 方法,将 message放到 MessageQueue中

  • 主线程创建好之后,会执行Looper.loop方法,该方法中获取与线程绑定的Looper对象,继而获取 MessageQueue,并开启一个会阻塞(释放CPU)的死循环,只要MessageQueue中还有message,就会获取该message,并执行 message.target.dispatchMessage处理消息。

相关推荐
Momo__5 小时前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富6 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇6 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇6 小时前
React中的forwardRef
前端·react.js·面试
槑有老呆6 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马6 小时前
Verilog开发常见问题汇总解析
前端
子兮曰6 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端
weedsfly6 小时前
语法糖褪去之后——Babel 转译产物中的 JavaScript 本貌
前端·javascript
JustHappy6 小时前
「软件设计思想杂谈🤔」“切图仔”也能懂编译原理?框架源码也许没那么难。聊聊 Vue 的编译(上)
前端·javascript·vue.js