Handler到底是一个什么东东

作为一个Android开发工程师,Handler简直是必须要了解的东西。每次面试前,Handler都会悄悄地钻到耳边对我说:“嘿,哥们,老地方见!”

果然,面试又问到了,而Handler又跑过来BB:“又被我难倒了吧!”

(内心独白,老子就不信搞不定你!)

于是便有了这篇解析。

先来几个问题

  1. 你了解Android的Handler机制吗?(我美不呲呵的说,那必须了解,一顿白话之后就有了下面的问题)
  2. 你知道Handler在安卓framework中有哪些应用吗?
  3. 你知道Handler中的同步屏障吗?
  4. 为什么在通过view.post()获取到的view宽高是准确的?post的消息不会出现在测量流程之后吗?
  5. 巴拉巴拉巴拉巴拉

行吧,在听到第一个问题的时候的信心满满到最后一个问题的灰心落寞,直接被打击的透透的。

为什么我要了解那么多,会写不就行了?

确实啊,我会用handler不就行了?作为一个画UI的,我有必要了解那么多吗?

但是我的理解是这样的,对底层了解的越透彻,写出bug的可能性就越低,因为老子知道怎么回事儿了呀。还有就是能了解能力边界,知道Handler能做什么不能做什么,这为开发其他技术方案有很大帮助。

话不多说,接下来,就要把「Handler」这王八犊子好好地扒一扒。

目录

  1. Android中的消息处理系统
  2. Handler是如何转起来的
  3. Handler在Android系统中的身影
  4. Handler的能力边界(这王八犊子都能用来做什么)

Android中的消息处理系统

在很多桌面操作系统中,都有自己的 消息处理框架,比如Windows、Android。

那么最基本的,消息处理框架,至少得有消息发送方(handler)、消息接收方(handler)、消息本身(Message)。

当消息生产速度非常快时,还需要一个存储方对消息进行暂时缓存(MessageQueue)。

而消息不是直达目标本身时,需要中间的一个调度中心(Looper),分别处理消息,方便统一调度。

这里举一个现实的例子:送快递

  1. 发快递方把物品打包,把包裹交给快递公司。
  2. 快递公司将很多人要发送的包裹统一装车运送到目标城市。
  3. 快递到达时,再将包裹分别运送给对应的收件方

当然有人说了,我就要用闪送,一次只送我的!那我只能说,你牛笔。

上面的案例中:

  1. handler就相当于一个快递员。负责收、发快递。
  2. Message就是一个快递。当然了快递也分(专送快递、普通快递、空包)
  3. MessageQueue就是一个存储快递的仓库
  4. Looper就是用来把每个快递分发给对应的快递员的,可以当做快递公司。
  5. 发快递方就是线程A
  6. 收件方就是线程B

当然了handler不仅局限于线程间通信。

Handler是如何转起来的

tips:基于API 30

接下来上一幅简单的小图来展示Handler是如何转起来的。

虽然这个图不是非常标准,但是它确实也描述了Handler的一整个工作机制。

那么我们就知道了,实际上除了Handler之外还有其他的几个角色,这里也让他们一并亮相吧。

  • Looper:用于开启循环,处理消息,相当于大总管。
  • MessageQueue:存储消息(实际上主要是操作Message链表),读取消息
  • Message:消息的封装,可以跨进程通信,本质是单向链表结构,并且会持有Handler(目标handler,其实就是发消息的Handler本身)。
  • Handler:用于发消息和处理消息的快递员。

那么他们真正的工作流程是什么样的呢?

如何在子线程中使用Handler

thread {// 1、创建Looper,必须创建了Looper,才能创建Handler Looper.prepare()// 2、创建Handlerhandler2 = Handler()// 3、让Looper进入循环状态Looper.loop()
}.start()

总结下来的步骤就是:

  1. 创建Looper,Looper.prepare()。
  2. 创建Handler,用于发消息和接收消息。
  3. 进入循环,Looper.loop()。

接下来挨个步骤开始,深入源码,进行分析。

创建Looper

想要发送消息,先得有一个Looper才行。那么为什么一定要Looper.prepare();

那我们便深入源码看看。

发现Looper只有一个私有的构造方法:

private Looper(boolean quitAllowed) {// 创建MessageQueue,传入是否可退出,啥?还有不可退出的?mQueue = new MessageQueue(quitAllowed);mThread = Thread.currentThread();
}

奥,原来Looper没有提供可直接创建对象的方法呀,(要不我通过反射自己创建一个吧!你可以试试!)

那么我们再来看看prepare方法

public static void prepare() {prepare(true);
}private static void prepare(boolean quitAllowed) {// 判断当前线程中是否已经存在Looper对象,若存在则报错。if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}// 创建Looper对象并放入内部的静态对象sThreadLocal中。sThreadLocal.set(new Looper(quitAllowed));
}

由prepare方法发现,原来Looper提供了这个prepare方法,方便开发者创建Looper对象,那么它为什么要自己创建,而且只允许一个线程中创建一个呢?

Looper是死循环获取消息的,所以创建多个没有意义,白白浪费空间罢了。而这里我们看到了传递进来的quitAllowed默认也都是true,而可传递参数的prepare方法又是私有的,没法直接调用到。

内部又使用了ThreadLocal对象存储了Looper对象,可以方便线程中其他方法直接通过Looper.myLooper()获取对象。

创建Handler

Handler不像Looper,它主要通过获取当前线程中的Looper或者指定的Looper,来进行消息发送。

所以创建Handler时,可以指定Looper,也可以不指定(不指定时,当前线程汇总必须得有Looper对象),

在调用了Handler的构造方法(非主动传递Looper对象)之后,最终会调用到如下 构造方法:public Handler(@Nullable Callback callback, boolean async) {.......mLooper = Looper.myLooper();if (mLooper == null) {throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread()+ " that has not called Looper.prepare()");}mQueue = mLooper.mQueue;mCallback = callback;mAsynchronous = async;
}

这里我们发现Handler中的构造方法中会存在一个async参数,当这个参数为true时,handler会在调用发送消息的方法时,最终会将Message设置为异步消息。

if (mAsynchronous) {msg.setAsynchronous(true);
}

然而,作为App开发者,我们是没有直接的接口可以设置这个参数的,那么我们也可以通过Message直接手动设置异步消息。

Message().apply {// 但是该方法是在API22之后才可以用isAsynchronous = true
}

进入循环状态

当调用了Looper.loop();整个消息处理处理算是打通了。

等等什么就打通了,你说的那个什么MessageQueue呢?那玩意不也需要创建么?

MessageQueue其实会在Looper创建时已经被创建了,作为Looper对象的成员存在。

那loop到底干了点啥?

public static void loop() {// 判断是否已经创建Looperfinal Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}if (me.mInLoop) {Slog.w(TAG, "Loop again would have the queued messages be executed"+ " before this one completed.");}me.mInLoop = true;final MessageQueue queue = me.mQueue;// 进入死循环for (;;) {// 从消息队列中拿消息。Message msg = queue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return;}// 获取观察者,咦,这个观察者是什么?我是不是可以添加一个观察者?不行至少常规方法不行。因为它是@hide的final Observer observer = sObserver;Object token = null;if (observer != null) {token = observer.messageDispatchStarting();}try {// 拿到消息之后,通过msg中的target进行消息分发。msg.target.dispatchMessage(msg);if (observer != null) {observer.messageDispatched(token, msg);}} catch (Exception exception) {if (observer != null) {observer.dispatchingThrewException(token, msg, exception);}throw exception;} finally {ThreadLocalWorkSource.restore(origWorkSource);if (traceTag != 0) {Trace.traceEnd(traceTag);}}// 回收消息msg.recycleUnchecked();}
}

由以上代码可知,Looper.loop()最本质的行为,就是不断通过queue.next();从queue中获取Message并且处理。

那么queue.next的这个方法到底有什么特别的呢?

Message next() {// Return here if the message loop has already quit and been disposed.// This can happen if the application tries to restart a looper after quit// which is not supported.final long ptr = mPtr;if (ptr == 0) {return null;}int pendingIdleHandlerCount = -1; // -1 only during first iterationint nextPollTimeoutMillis = 0;// ① 进入死循环,咦这里为什么是死循环?for (;;) {if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}//② 这里就是进入等待状态,如果是进入等待状态为什么不用wait()?而用native的epoll方法?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;// ③ 如果target == null,则进入循环,怎么target可以为null吗?不是通过Handler发送消息的时候默认设置的吗?if (msg != null && msg.target == null) {// Stalled by a barrier.  Find the next asynchronous message in the queue.do {prevMsg = msg;msg = msg.next;// ④ 如果msg不为空,且不是异步消息,奥,原来这里是为了找到异步消息。} 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;// 只有在找到了异步消息的时候这里才会不为null吧if (prevMsg != null) {prevMsg.next = msg.next;} else {// 这里就让下一个做头部Message了mMessages = msg.next;}msg.next = null;if (DEBUG) Log.v(TAG, "Returning message: " + msg);msg.markInUse();return msg;}} else {// No more messages.nextPollTimeoutMillis = -1;}// Process the quit message now that all pending messages have been handled.// 如果要退出的话,就直接退出。if (mQuitting) {dispose();return null;}……}}

针对上面的代码,虽然我们都能看得懂,但是还是会有这样那样的疑问,接下来我们一起来分析一下这几个问题。

① 进入死循环,咦这里为什么是死循环?

我们知道在Looper中,已经出现一个死循环了,难道通过那个死循环还不够吗?按照我的想法,应该直接在那里直接进行死循环就行了呀,如果存在Message就执行,不存在就直接退出就行了。

首先直接退出肯定不行,万一后面还有消息要来呢,所以此处肯定不能直接退出。那么为什么再放到MessageQueue中获取呢?

MessageQueue只是获取下一个Message,并将Message返回,Looper负责处理。这样有利于责任划分,符合单一职责划分。

而当MessageQueue中取不到数据时,需要再次获取,所以要进入死循环,直到获取到那个Message。

更主要的问题是,主线程不能退出,所以通过死循环的方式可以让主线程一直处于运行状态。

② 这里就是进入等待状态,如果是进入等待状态为什么不用wait()?而用native的epoll方法?

首先wait,是监视器用来控制线程状态的,是控制多线程访问共享资源时的方法,并不能让当前线程进入等待状态。

使用native.epoll方法可以让线程进入等待状态。

③ 如果Message的target == null,则进入循环,怎么target可以为null吗?不是通过Handler发送消息的时候默认设置的吗?

首先来说,target==null的消息有什么特别吗?当target==null时,该消息为同步屏障消息。

在google提供的API中,我们确实不能直接发送target为空的消息。

当同步屏障消息出现时,mq会读取当前屏障消息后面的异步消息,优先处理异步消息,直到同步屏障被移除。

⑥ 对呀还要时间判断,那我其他设置了延时的消息怎么办?

为什么直接到6了?你猜。

延时的消息是在哪里设置的?如果超过了我设置的延时时间怎么办?

其实是在handler发送消息,消息进入队列时,就已经根据message的执行时间点排序好了。

还有一个问题,怎么保证消息的延迟时间是正确的?如果我修改了系统时间呢?

当Message设置when属性时,是通过SystemClock.updateMills()来获取的,获取的是系统开启到现在的执行时间,所以不会跟随系统时间改变而改变,所以时间就是准确的了。

Handler在Android系统中的身影

如果读过framework源码的都知道,在Android中有一个非常重要的类,叫 ActivityThead,在它里面有这样一个H类。它就是handler的子类里面记录了包含了很多系统消息的处理。

class H extends Handler {……public void handleMessage(Message msg) {if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));switch (msg.what) {case BIND_APPLICATION:Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");AppBindData data = (AppBindData)msg.obj;handleBindApplication(data);Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;case EXIT_APPLICATION:if (mInitialApplication != null) {mInitialApplication.onTerminate();}Looper.myLooper().quit();break;case RECEIVER:Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastReceiveComp");handleReceiver((ReceiverData)msg.obj);Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;case CREATE_SERVICE:if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,("serviceCreate: " + String.valueOf(msg.obj)));}handleCreateService((CreateServiceData)msg.obj);Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;case BIND_SERVICE:Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind");handleBindService((BindServiceData)msg.obj);Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;case UNBIND_SERVICE:Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceUnbind");handleUnbindService((BindServiceData)msg.obj);schedulePurgeIdler();Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;……}……}……
}

还有当我们在调用View.post方法时,最终也会调用到Handler的Post方法,那么这个Handler是哪里来的呢?
在这个Handler是ViewRootImpl中传递过来的。而在ViewRootImpl中也有一个自定义的Handler,这个Handler主要用于做什么,大家可以自行探讨。我们发现这个Handler创建的时候,没有调用Looper.prepare()这是为什么呢?因为它拿到的是主线程的Looper啦。

final class ViewRootHandler extends Handler {……
}

handler的能力边界

首先来说它只是一个跨线程、进程通信的工具。那么它当然能用来跨线程、跨进程通信了。

那么通过它在Android中身份,和各种Handler所执行的行为,还能做些什么呢?

比如某大神通过Handler机制开发出了BlockCanary这种工具,来检测代码执行效率。

有些框架通过获取主线程Looper实现了主、子线程切换。

还有人实现了在子线程弹Toast。

那它的能力边界到底在哪呢?只能说,我也不知道~~~

其他小Tips

同步屏障

同步屏障这个问题已经被问到过很多次了。

在Handler中存在发送移除同步屏障方法。postSyncBarrier()、removeSyncBarrier()

在添加同步屏障之后,handler在碰到第一个屏障消息(target==null的那个消息)之后,会优先读取屏障消息后面的异步消息进行优先执行。

那么再Android中哪里有用到呢,毕竟这个方法我们不能直接通过api调用。

在 View 更新时,draw、requestLayout、invalidate 等很多地方都调用了ViewRootImpl#scheduleTraversals()

由于 UI 更新相关的消息是优先级最高的。

Handler怎么做到准确的计时的?

通过SystemClock来获取的时间,该时间为手机启动开始到现在的运行时间,不会随着系统时间的改变而改变。

View.post为什么能获取到准确宽高,如果它post的事件在绘制之前呢?

因为同步屏障会优先执行绘制代码。

好了可以了,今天先BB这么多,等再多看看源码,多总结总结~~

Handler到底是一个什么东东相关推荐

  1. 刀片服务器改台式电脑_服务器到底是个什么东东?跟电脑有啥区别?电脑知识学习!...

    一位朋友留言点的内容,想了解服务器方面的知识,对于普通用户而言,确实对服务器感觉很神秘,不知道服务器到底是个什么东东,我保证看完这篇,你就会明白服务器到底是个啥了. 首先可以很明确的告诉你,服务器也是 ...

  2. 服务器到底是个什么东东?跟电脑有啥区别?

    一位朋友留言点的内容,想了解服务器方面的知识,对于普通用户而言,确实对服务器感觉很神秘,不知道服务器到底是个什么东东,我保证看完这篇,你就会明白服务器到底是个啥了. 首先可以很明确的告诉你,服务器也是 ...

  3. 一个超牛的东东:专门删除牛皮文件和文件夹

    一个超牛的东东:专门删除牛皮文件和文件夹 电脑中有一些不知为何删除不掉的文件和文件夹,无论用什么超级粉碎机都无法清除. 在网上看到的一个方法可以帮忙 注意:使用时请小心, 建立非常简单: 新建文本文档 ...

  4. JavaScript怎么安装_几句话说清楚JavaScript、V8引擎、NodeJS、NMP,到底是什么东东...

    小程序开发如火如荼,如果你是程序员,你还不懂小程序的开发,恐怕会被同行认为太LOW了吧!不过,新入行小程序开发者确实会被新的名词搞得一头雾水. 比如JavaScript不是在浏览器端运行吗,怎么还可以 ...

  5. 一个祸害我很久的东东——事件过滤器之按键触发

    一个祸害我很久的东东--事件过滤器之按键触发 下面这个东东其实很常见,也很实用,平时上网的时候对之经常见,以为很简单,当然弄懂后,其实发现,他确实蛮简单的,但就是这小东西害了我好久好久啊.... 就是 ...

  6. yahoo的yui是一个好东东

    yahoo的yui是一个好东东 最近在公司一直在做JS方面的工作,因为工作需要,使用了yahoo的yui脚本库,感觉相当的好.不过有一些地方是要注意一下的,那就是this指针方面.在使用它的conne ...

  7. 京东笔试——神奇数 【题目描述】东东在一本古籍上看到有一种神奇数,如果能够将一个数的数字分成两组,其中一组数字的和 等于另一组数字的和,我们就将这个数称为神奇数。例如 242 就是一个神奇数,我们能够

    2 .神奇数 [ 题目描述]东东在一本古籍上看到有一种神奇数,如果能够将一个数的数字分成两组,其中一组数字的和 等于另一组数字的和,我们就将这个数称为神奇数.例如 242 就是一个神奇数,我们能够将这 ...

  8. Google Chrome Frame一个神奇的东东

    Google Chrome Frame是一个神奇的东东 只需要在网站中写上,<meta http-equiv="X-UA-Compatible" content=" ...

  9. latte到底是个什么东东?

    早上打开音乐播放器听到的第一句就是 "想用一杯latte把你灌醉",有点疑惑:latte是个什么东东?就上网搜了下,结果在baidu贴吧找到一条: latte   李圣杰<痴 ...

  10. 又一个有创意的新东东: 基于手势的遥控棒

    <script type="text/javascript"></script> <script src="http://pagead2.g ...

最新文章

  1. 王道408数据结构——第一章 绪论
  2. 跟我学 Java 8 新特性之 Stream 流(六)收集
  3. 阿里新一代分布式任务调度平台Schedulerx2.0破土而出
  4. win10 Java JDK环境变量配置
  5. HDU 4647 Another Graph Game
  6. ios怎么创建html文件夹,ios 创建html文件
  7. Css3新特性应用之形状
  8. 泛微发布亿元补贴计划,推动移动办公普及
  9. Vue 之 .eslintrc.js 文件
  10. 微信小程序嵌套h5页面+发布微信小程序(超级简单)
  11. 计算机网络浅谈,浅谈计算机网络的重要性
  12. mac 安装mysql 后设置开机自启
  13. CentOS7.6(1810)安装
  14. 计算机电影制作专业,影视制作专业是学什么的
  15. 2019 Revit二次开发企业
  16. logit模型应用实例_广义线性模型应用举例之beta回归及R计算
  17. Linux系统重启和停止Mysql服务教程
  18. 量化交易入门阶段:布林带调整参数又如何?
  19. oracle数字转换成人民币大写
  20. 洗脑与教育,独立思考,自我的划界

热门文章

  1. PHP开发的一个搞笑段子手生成小程序
  2. Linux应该怎么快速学习?首推这份全网爆火的“Linux速成笔记”,阿里架构师都在用它!
  3. Python中form的使用
  4. 403错误服务器未响应是什么意思,什么是HTTP ERROR 403?导致403错误的主要原因及解决方法...
  5. 存储系统结构、MDR、MBR、扇区
  6. vue+element-ui之表格中如何插入图片链接
  7. 插画师如何确定自己的风格?教你如何一步步找到自己绘画风格!
  8. 2022-2028年中国手机银行行业市场竞争态势及未来前景分析报告
  9. 下厨房app竞品分析(产品和用户)
  10. python适用的操作系统是_python 操作系统和进程