你真的懂Handler.postDelayed()的原理吗?
转载自http://www.dss886.com/2016/08/17/01/
使用handler发送消息时有两种方式,post(Runnable r)和post(Runnable r, long delayMillis)都是将指定Runnable(包装成PostMessage)加入到MessageQueue中,然后Looper不断从MessageQueue中读取Message进行处理。
然而我在使用的时候就一直有一个疑问,类似Looper这种「轮询」的工作方式,如果在每次读取时判断时间,是无论如何都会有误差的。但是在测试中发现Delay的误差并没有大于我使用System.out.println(System.currentTimeMillis())所产生的误差,几乎可以忽略不计,那么Android是怎么做到的呢?
Handler.postDelayed()的调用路径
一步一步跟一下Handler.postDelayed()的调用路径:
- Handler.postDelayed(Runnable r, long delayMillis)
- Handler.sendMessageDelayed(getPostMessage(r), delayMillis)
- Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)
- Handler.enqueueMessage(queue, msg, uptimeMillis)
- MessageQueue.enqueueMessage(msg, uptimeMillis)
最后发现Handler没有自己处理Delay,而是交给了MessageQueue处理,我们继续跟进去看看MessageQueue又做了什么:
- msg.markInUse();
- msg.when = when;
- Message p = mMessages;
- boolean needWake;
- if (p == null || when == 0 || when < p.when) {
- // New head, wake up the event queue if blocked.
- msg.next = p;
- mMessages = msg;
- needWake = mBlocked;
- } else {
- ...
- }
MessageQueue中组织Message的结构就是一个简单的单向链表,只保存了链表头部的引用(果然只是个Queue啊)。在enqueueMessage()的时候把应该执行的时间(上面Hanlder调用路径的第三步延迟已经加上了现有时间,所以叫when)设置到msg里面,并没有进行处理……WTF?
继续跟进去看看Looper是怎么读取MessageQueue的,在loop()方法内:
- for (;;) {
- Message msg = queue.next(); // might block
- if (msg == null) {
- // No message indicates that the message queue is quitting.
- return;
- }
- ...
- }
原来调用的是MessageQueue.next(),还贴心地注释了这个方法可能会阻塞,点进去看看:
- for (;;) {
- if (nextPollTimeoutMillis != 0) {
- Binder.flushPendingCommands();
- }
- nativePollOnce(ptr, nextPollTimeoutMillis);
- synchronized (this) {
- // Try to retrieve the next message. Return if found.
- final long now = SystemClock.uptimeMillis();
- Message prevMsg = null;
- Message msg = mMessages;
- if (msg != null && msg.target == null) {
- // Stalled by a barrier. Find the next asynchronous message in the queue.
- do {
- prevMsg = msg;
- msg = msg.next;
- } while (msg != null && !msg.isAsynchronous());
- }
- if (msg != null) {
- if (now < msg.when) {
- // Next message is not ready. Set a timeout to wake up when it is ready.
- nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
- } else {
- // Got a message.
- mBlocked = false;
- if (prevMsg != null) {
- prevMsg.next = msg.next;
- } else {
- mMessages = msg.next;
- }
- msg.next = null;
- if (DEBUG) Log.v(TAG, "Returning message: " + msg);
- msg.markInUse();
- return msg;
- }
- } else {
- // No more messages.
- nextPollTimeoutMillis = -1;
- }
- ...
- }
- }
可以看到,在这个方法内,如果头部的这个Message是有延迟而且延迟时间没到的(now < msg.when),会计算一下时间(保存为变量nextPollTimeoutMillis),然后在循环开始的时候判断如果这个Message有延迟,就调用nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞。nativePollOnce()的作用类似与object.wait(),只不过是使用了Native的方法对这个线程精确时间的唤醒。
精确延时的问题到这里就算是基本解决了,不过我又产生了一个新的疑问:如果Message会阻塞MessageQueue的话,那么先postDelay10秒一个Runnable A,消息队列会一直阻塞,然后我再post一个Runnable B,B岂不是会等A执行完了再执行?正常使用时显然不是这样的,那么问题出在哪呢?
再来一步一步顺一下Looper、Handler、MessageQueue的调用执行逻辑,重新看到MessageQueue.enqueueMessage()的时候发现,似乎刚才遗漏了什么东西:
- msg.markInUse();
- msg.when = when;
- Message p = mMessages;
- boolean needWake;
- if (p == null || when == 0 || when < p.when) {
- // New head, wake up the event queue if blocked.
- msg.next = p;
- mMessages = msg;
- needWake = mBlocked;
- } else {
- ...
- }
- ...
- // We can assume mPtr != 0 because mQuitting is false.
- if (needWake) {
- nativeWake(mPtr);
- }
这个needWake变量和nativeWake()方法似乎是唤醒线程啊?继续看看mBlocked是什么:
- Message next() {
- for (;;) {
- ...
- if (msg != null) {
- ...
- } else {
- // Got a message.
- mBlocked = false;
- ...
- }
- ...
- }
- ...
- if (pendingIdleHandlerCount <= 0) {
- // No idle handlers to run. Loop and wait some more.
- mBlocked = true;
- continue;
- }
- ...
- }
就是这里了,在next()方法内部,如果有阻塞(没有消息了或者只有Delay的消息),会把mBlocked这个变量标记为true,在下一个Message进队时会判断这个message的位置,如果在队首就会调用nativeWake()方法唤醒线程!
现在整个调用流程就比较清晰了,以刚刚的问题为例:
postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;- 紧接着
post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程; MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;- Looper处理完这个消息再次调用
next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞; - 直到阻塞时间到或者下一次有Message进队;
这样,基本上就能保证Handler.postDelayed()发布的消息能在相对精确的时间被传递给Looper进行处理而又不会阻塞队列了。
你真的懂Handler.postDelayed()的原理吗?的更多相关文章
- javascript的语法作用域你真的懂了吗
原文:javascript的语法作用域你真的懂了吗 有段时间没有更新了,思绪一下子有点转不过来.正应了一句古话“一天不读书,无人看得出:一周不读书,开始会爆粗:一月不读书,智商输给猪.”.再加上周五晚 ...
- 你真的懂ajax吗?
前言 总括: 本文讲解了ajax的历史,工作原理以及优缺点,对XMLHttpRequest对象进行了详细的讲解,并使用原生js实现了一个ajax对象以方便日常开始使用. damonare的ajax库: ...
- 面试:Handler 的工作原理是怎样的?
面试场景 平时开发用到其他线程吗?都是如何处理的? 基本都用 RxJava 的线程调度切换,嗯对,就是那个 observeOn 和 subscribeOn 可以直接处理,比如网络操作,RxJava 提 ...
- Android中三种计时器Timer、CountDownTimer、handler.postDelayed的使用
在android开发中,我们常常需要用到计时器,倒计时多少秒后再执行相应的功能,下面我就分别来讲讲这三种常用的计时的方法. 一.CountDownTimer 该类是个抽象类,如果要使用这个类中的方法, ...
- 一篇读懂HTTPS:加密原理、安全逻辑、数字证书等
1.引言 HTTPS(全称: Hypertext Transfer Protocol Secure,超文本传输安全协议),是以安全为目标的HTTP通道,简单讲是HTTP的安全版.本文,就来深入介绍下其 ...
- “三次握手,四次挥手”你真的懂吗?TCP
“三次握手,四次挥手”你真的懂吗? mp.weixin.qq.com 来源:码农桃花源 解读:“拼多多”被薅的问题出在哪儿?损失将如何买单? 之前有推过一篇不错的干货<TCP之三次握手四次挥手 ...
- 你真的懂 ajax 吗?
前言 总括: 本文讲解了ajax的历史,工作原理以及优缺点,对XMLHttpRequest对象进行了详细的讲解,并使用原生js实现了一个ajax对象以方便日常开始使用. damonare的ajax库: ...
- 程序猿修仙之路--数据结构之你是否真的懂数组? c#socket TCP同步网络通信 用lambda表达式树替代反射 ASP.NET MVC如何做一个简单的非法登录拦截
程序猿修仙之路--数据结构之你是否真的懂数组? 数据结构 但凡IT江湖侠士,算法与数据结构为必修之课.早有前辈已经明确指出:程序=算法+数据结构 .要想在之后的江湖历练中通关,数据结构必不可少. ...
- [转帖]看完这篇文章,我奶奶都懂了https的原理
看完这篇文章,我奶奶都懂了https的原理 http://www.17coding.info/article/22 非对称算法 以及 CA证书 公钥 核心是 大的质数不一分解 还有 就是 椭圆曲线算法 ...
随机推荐
- 《Redis设计与实现》学习笔记
第2章 简单动态字符串(SDS) redis的字符串不是直接用c语言的字符串,而是用了一种称为简单动态字符串(SDS)的抽象类型,并将其作为默认字符串. redis中包含字符串值的键值对在底层都是由S ...
- vue1.0到2.0
vue1.0到2.0 vue2.0 新手教程(一) 想想自己写vue的项目也写了一年了,从vue1.0到2.0,走过不少路,填过不少坑, 下面记录一下新手从0到1的过程,本文“应该”会持续更新 首 ...
- 【LOJ】#2536. 「CQOI2018」解锁屏幕
题解 什么破题,看一眼就能想出来\(n^2 2^n\)看了一眼数据范围有点虚,结果跑得飞快= = 处理出\(a[i][j]\)表示从\(i\)到\(j\)经过的点的点集 然后\(f[i][S]\)表示 ...
- DJANGO ADMIN 一些有用的设置(转)
DJANGO ADMIN 一些有用的设置 Django自带的后台管理是Django明显特色之一,可以让我们快速便捷管理数据.后台管理可以在各个app的admin.py文件中进行控制.以下是我最近摸 ...
- [转]SPFA算法的玄学方法
最近想到了许多优化spfa的方法,这里想写个日报与大家探讨下 前置知识:spfa(不带任何优化) 由于使用较多 STLSTL ,本文中所有代码的评测均开启 O_2O2 优化 对一些数组的定义: di ...
- Nessus漏洞扫描教程之使用Nmap工具扫描识别指纹
Nessus漏洞扫描教程之使用Nmap工具扫描识别指纹 Nmap工具的准备工作 当用户对Nessus工具有清晰的认识后,即可使用该工具实施扫描.但是,在扫描之前需要做一些准备工作,如探测网络中活动的主 ...
- 快速排序之Java实现
快速排序之Java实现 代码: package cn.com.zfc.lesson21.sort; /** * * @title QuickSort * @describe 快速排序 * @autho ...
- [BZOJ3757]苹果树(树上莫队)
树上莫队共有三种写法: 1.按DFS序列分块,和普通莫队类似.常数大,不会被卡. 2.按块状树的方式分块.常数小,会被菊花图卡到O(n). 3.按[BZOJ1086]王室联邦的方式分块.常数小,不会被 ...
- BZOJ1171 : 大sz的游戏
f[i]=min(f[j])+1,线段j与线段i有交,且l[i]-l[j]<=L. 线段j与线段i有交等价于y[j]>=x[i],x[j]<=y[i]. 因为l[i]递增,所以可以维 ...
- Xtreme9.0 - Communities 强连通
Xtreme9.0 - Communities 题目连接: https://www.hackerrank.com/contests/ieeextreme-challenges/challenges/c ...