android handler封装_Handler都没搞懂,你拿什么去跳槽啊?!

6c5ab58fde635d5a419c17d2115c4f23.png

0. 前言

做 Android 开发肯定离不开跟 Handler 打交道,它通常被我们用来做主线程与子线程之间的通信工具,而 Handler 作为 Android 中消息机制的重要一员也确实给我们的开发带来了极大的便利。

Handler应用之广泛,可以说只要有异步线程与主线程通信的地方就一定会有 Handler。

所以搞懂 Handler 对理解Android非常有必要。

那么,Handler 的通信机制的背后的原理是什么?

本文带你揭晓。

注意:本文所展示的系统源码基于 Android-27 ,并有所删减。

1. 重识 Handler

我们可以使用 Handler 发送并处理与一个线程关联的 Message 和 Runnable 。(注意:Runnable 会被封装进一个 Message,所以它本质上还是一个 Message )

每个 Handler 都会跟一个线程绑定,并与该线程的 MessageQueue 关联在一起,从而实现消息的管理以及线程间通信。

1.1 Handler 的基本用法

1android.os.Handler handler = new Handler(){2 @Override3 public void handleMessage(final Message msg) {4 //这里接受并处理消息5 }6};7//发送消息8handler.sendMessage(message);9handler.post(runnable);

实例化一个 Handler 重写 handleMessage 方法 ,然后在需要的时候调用它的 send 以及 post 系列方法就可以了,非常简单易用,并且支持延时消息。(更多方法可查询 API 文档)

但是奇怪,我们并没有看到任何 MessageQueue 的身影,也没看到它与线程绑定的逻辑,这是怎么回事?

2. Handler 原理解析

相信大家早就听说过了 Looper 以及 MessageQueue 了,我就不多绕弯子了。

不过在开始分析原理之前,先明确我们的问题:

  1. Handler 是如何与线程关联的?
  2. Handler 发出去的消息是谁管理的?
  3. 消息又是怎么回到 handleMessage() 方法的?
  4. 线程的切换是怎么回事?

2.1 Handler 与 Looper 的关联

实际上我们在实例化 Handler 的时候 Handler 会去检查当前线程的 Looper 是否存在,如果不存在则会报异常,也就是说在创建 Handler 之前一定需要先创建 Looper 。

代码如下:

 1public Handler(Callback callback, boolean async) { 2 //检查当前的线程是否有 Looper 3 mLooper = Looper.myLooper(); 4 if (mLooper == null) { 5 throw new RuntimeException( 6 "Can't create handler inside thread that has not called Looper.prepare()"); 7 } 8 //Looper 持有一个 MessageQueue 9 mQueue = mLooper.mQueue;10}

这个异常相信很多同学遇到过,而我们平时直接使用感受不到这个异常是因为主线程已经为我们创建好了 Looper,先记住,后面会讲。(见【3.2】)

一个完整的 Handler 使用例子其实是这样的:

 1class LooperThread extends Thread { 2 public Handler mHandler; 3 public void run() { 4 Looper.prepare(); 5 mHandler = new Handler() { 6 public void handleMessage(Message msg) { 7 // process incoming messages here 8 } 9 };10 Looper.loop();11 }12}

Looper.prepare() :

1//Looper2private static void prepare(boolean quitAllowed) {3 if (sThreadLocal.get() != null) {4 throw new RuntimeException("Only one Looper may be created per thread");5 }6 sThreadLocal.set(new Looper(quitAllowed));7}

Looper 提供了 Looper.prepare() 方法来创建 Looper ,并且会借助 ThreadLocal 来实现与当前线程的绑定功能。Looper.loop() 则会开始不断尝试从 MessageQueue 中获取 Message , 并分发给对应的 Handler(见【2.3】)。

也就是说 Handler 跟线程的关联是靠 Looper 来实现的。

2.2 Message 的存储与管理

Handler 提供了一些列的方法让我们来发送消息,如 send()系列 post()系列 。

不过不管我们调用什么方法,最终都会走到 Message.enqueueMessage(Message,long) 方法。

以 sendEmptyMessage(int) 方法为例:

1//Handler2sendEmptyMessage(int)3 -> sendEmptyMessageDelayed(int,int)4 -> sendMessageAtTime(Message,long)5 -> enqueueMessage(MessageQueue,Message,long)6 -> queue.enqueueMessage(Message, long);

到了这里,消息的管理者 MessageQueue 也就露出了水面。

MessageQueue 顾明思议,就是个队列,负责消息的入队出队。

2.3 Message 的分发与处理

了解清楚 Message 的发送与存储管理后,就该揭开分发与处理的面纱了。

前面说到了 Looper.loop() 负责对消息的分发,本章节进行分析。

先来看看所涉及到的方法:

 1//Looper 2public static void loop() { 3 final Looper me = myLooper(); 4 if (me == null) { 5 throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); 6 } 7 final MessageQueue queue = me.mQueue; 8 //... 9 for (;;) {10 // 不断从 MessageQueue 获取 消息11 Message msg = queue.next(); // might block12 //退出 Looper 13 if (msg == null) {14 // No message indicates that the message queue is quitting.15 return;16 }17 //...18 try {19 msg.target.dispatchMessage(msg);20 end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();21 } finally {22 //...23 }24 //...25 //回收 message, 见【3.5】26 msg.recycleUnchecked();27 }28}

loop() 里调用了 MessageQueue.next() :

 1//MessageQueue 2Message next() { 3 //... 4 for (;;) { 5 //... 6 nativePollOnce(ptr, nextPollTimeoutMillis); 7 8 synchronized (this) { 9 // Try to retrieve the next message. Return if found.10 final long now = SystemClock.uptimeMillis();11 Message prevMsg = null;12 Message msg = mMessages;13 //...14 if (msg != null) {15 if (now < msg.when) {16 // Next message is not ready. Set a timeout to wake up when it is ready.17 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);18 } else {19 // Got a message.20 mBlocked = false;21 if (prevMsg != null) {22 prevMsg.next = msg.next;23 } else {24 mMessages = msg.next;25 }26 msg.next = null;27 return msg;28 }29 } else {30 // No more messages.31 nextPollTimeoutMillis = -1;32 }3334 // Process the quit message now that all pending messages have been handled.35 if (mQuitting) {36 dispose();37 return null;38 }39 }4041 // Run the idle handlers. 关于 IdleHandler 自行了解42 //...43 }44}

还调用了 msg.target.dispatchMessage(msg) ,msg.target 就是发送该消息的 Handler,这样就回调到了 Handler 那边去了:

 1//Handler 2public void dispatchMessage(Message msg) { 3 //msg.callback 是 Runnable ,如果是 post方法则会走这个 if 4 if (msg.callback != null) { 5 handleCallback(msg); 6 } else { 7 //callback 见【3.4】 8 if (mCallback != null) { 9 if (mCallback.handleMessage(msg)) {10 return;11 }12 }13 //回调到 Handler 的 handleMessage 方法14 handleMessage(msg);15 }16}

注意:dispatchMessage() 方法针对 Runnable 的方法做了特殊处理,如果是 ,则会直接执行 Runnable.run() 。

分析:Looper.loop() 是个死循环,会不断调用 MessageQueue.next() 获取 Message ,并调用 msg.target.dispatchMessage(msg) 回到了 Handler 来分发消息,以此来完成消息的回调。

注意:loop()方法并不会卡死主线程,见【6】。

那么线程的切换又是怎么回事呢?

很多人搞不懂这个原理,但是其实非常简单,我们将所涉及的方法调用栈画出来,如下:

1Thread.foo(){2 Looper.loop()3 -> MessageQueue.next()4 -> Message.target.dispatchMessage()5 -> Handler.handleMessage()6}

显而易见,Handler.handleMessage() 所在的线程最终由调用 Looper.loop() 的线程所决定。

平时我们用的时候从异步线程发送消息到 Handler,这个 Handler 的 handleMessage() 方法是在主线程调用的,所以消息就从异步线程切换到了主线程。

2.3 图解原理

文字版的原理解析到这里就结束了,如果你看到这里还是没有懂,没关系,我特意给你们准备了些图,配合着前面几个章节,再多看几遍,一定可以吃透。

556a7ecf036d0a38089b1777f6ebe098.png

handler-looper-mq.jpg

1b451560cf6c6a383d86d1bc87bcd991.png

handler_java.jpg

图片来源见【6】

2.4 小结

Handler 的背后有着 Looper 以及 MessageQueue 的协助,三者通力合作,分工明确。

尝试小结一下它们的职责,如下:

  • Looper :负责关联线程以及消息的分发,会与创建它的线程绑定,并负责在该线程下从 MessageQueue 获取 Message,分发给 Handler ;
  • MessageQueue :是个队列,负责消息的存储与管理,负责管理由 Handler 发送过来的 Message ;
  • Handler : 负责发送并处理消息,面向开发者,提供 API,并隐藏背后实现的细节。

对【2】章节提出的问题用一句话总结:

Handler 发送的消息由 MessageQueue 存储管理,并由 Loopler 负责回调消息到 handleMessage()。

线程的转换由 Looper 完成,handleMessage() 所在线程由 Looper.loop() 调用者所在线程决定。

3. Handler 的延伸

Handler 虽然简单易用,但是要用好它还是需要注意一点,另外 Handler相关 还有些鲜为人知的知识技巧,比如 IdleHandler。

由于 Handler 的特性,它在 Android 里的应用非常广泛,比如: AsyncTask、HandlerThread、Messenger、IdleHandler 和 IntentService 等等。

这些我会讲解一些,我没讲到的可以自行搜索相关内容进行了解。

3.1 Handler 引起的内存泄露原因以及最佳解决方案

Handler 允许我们发送延时消息,如果在延时期间用户关闭了 Activity,那么该 Activity 会泄露。

这个泄露是因为 Message 会持有 Handler,而又因为 Java 的特性,内部类会持有外部类,使得 Activity 会被 Handler 持有,这样最终就导致 Activity 泄露。

解决该问题的最有效的方法是:将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用,并及时移除所有消息。

示例代码如下:

 1private static class SafeHandler extends Handler { 2 3 private WeakReference ref; 4 5 public SafeHandler(HandlerActivity activity) { 6 this.ref = new WeakReference(activity); 7 } 8 9 @Override10 public void handleMessage(final Message msg) {11 HandlerActivity activity = ref.get();12 if (activity != null) {13 activity.handleMessage(msg);14 }15 }16}

并且再在 Activity.onDestroy() 前移除消息,加一层保障:

1@Override2protected void onDestroy() {3 safeHandler.removeCallbacksAndMessages(null);4 super.onDestroy();5}

这样双重保障,就能完全避免内存泄露了。

注意:单纯的在 onDestroy 移除消息并不保险,因为 onDestroy 并不一定执行。

3.2 为什么我们能在主线程直接使用 Handler,而不需要创建 Looper ?

前面我们提到了每个Handler 的线程都有一个 Looper ,主线程当然也不例外,但是我们不曾准备过主线程的 Looper 而可以直接使用,这是为何?

注意:通常我们认为 ActivityThread 就是主线程。事实上它并不是一个线程,而是主线程操作的管理者,所以吧,我觉得把 ActivityThread 认为就是主线程无可厚非,另外主线程也可以说成 UI 线程。

在 ActivityThread.main() 方法中有如下代码:

 1//android.app.ActivityThread 2public static void main(String[] args) { 3 //... 4 Looper.prepareMainLooper(); 5 6 ActivityThread thread = new ActivityThread(); 7 thread.attach(false); 8 9 if (sMainThreadHandler == null) {10 sMainThreadHandler = thread.getHandler();11 }12 //...13 Looper.loop();1415 throw new RuntimeException("Main thread loop unexpectedly exited");16}

Looper.prepareMainLooper(); 代码如下:

 1/** 2 * Initialize the current thread as a looper, marking it as an 3 * application's main looper. The main looper for your application 4 * is created by the Android environment, so you should never need 5 * to call this function yourself. See also: {@link #prepare()} 6 */ 7public static void prepareMainLooper() { 8 prepare(false); 9 synchronized (Looper.class) {10 if (sMainLooper != null) {11 throw new IllegalStateException("The main Looper has already been prepared.");12 }13 sMainLooper = myLooper();14 }15}

可以看到在 ActivityThread 里 调用了 Looper.prepareMainLooper() 方法创建了 主线程的 Looper ,并且调用了 loop() 方法,所以我们就可以直接使用 Handler 了。

注意:Looper.loop() 是个死循环,后面的代码正常情况不会执行。

3.3 主线程的 Looper 不允许退出

如果你尝试退出 Looper ,你会得到以下错误信息:

1Caused by: java.lang.IllegalStateException: Main thread not allowed to quit.2 at android.os.MessageQueue.quit(MessageQueue.java:415)3 at android.os.Looper.quit(Looper.java:240)

why? 其实原因很简单,主线程不允许退出,退出就意味 APP 要挂。

3.4 Handler 里藏着的 Callback 能干什么?

在 Handler 的构造方法中有几个 要求传入 Callback ,那它是什么,又能做什么呢?

来看看 Handler.dispatchMessage(msg) 方法:

 1public void dispatchMessage(Message msg) { 2 //这里的 callback 是 Runnable 3 if (msg.callback != null) { 4 handleCallback(msg); 5 } else { 6 //如果 callback 处理了该 msg 并且返回 true, 就不会再回调 handleMessage 7 if (mCallback != null) { 8 if (mCallback.handleMessage(msg)) { 9 return;10 }11 }12 handleMessage(msg);13 }14}

可以看到 Handler.Callback 有优先处理消息的权利 ,当一条消息被 Callback 处理并拦截(返回 true),那么 Handler 的 handleMessage(msg) 方法就不会被调用了;如果 Callback 处理了消息,但是并没有拦截,那么就意味着一个消息可以同时被 Callback 以及 Handler 处理。

这个就很有意思了,这有什么作用呢?

我们可以利用 Callback 这个拦截机制来拦截 Handler 的消息!

场景:Hook ActivityThread.mH , 在 ActivityThread 中有个成员变量 mH ,它是个 Handler,又是个极其重要的类,几乎所有的插件化框架都使用了这个方法。

3.5 创建 Message 实例的最佳方式

由于 Handler 极为常用,所以为了节省开销,Android 给 Message 设计了回收机制,所以我们在使用的时候尽量复用 Message ,减少内存消耗。

方法有二:

  1. 通过 Message 的静态方法 Message.obtain(); 获取;
  2. 通过 Handler 的公有方法 handler.obtainMessage(); 。

3.6 子线程里弹 Toast 的正确姿势

当我们尝试在子线程里直接去弹 Toast 的时候,会 crash :

1java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() 2

本质上是因为 Toast 的实现依赖于 Handler,按子线程使用 Handler 的要求修改即可(见【2.1】),同理的还有 Dialog。

正确示例代码如下:

1new Thread(new Runnable() {2 @Override3 public void run() {4 Looper.prepare();5 Toast.makeText(HandlerActivity.this, "不会崩溃啦!

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

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

相关文章

ORA-12514: TNS:监听程序当前无法识别连接描述符中请(转)

http://blog.sina.com.cn/s/blog_628ed1290100gci9.html转载于:https://www.cnblogs.com/gaofei_work/archive/2010/05/19/1739174.html

做梦也想有一个这样的实验室

从现在开始&#xff0c;努力攒钱&#xff0c;等有钱了&#xff0c;自己也开一个这样的实验室。推荐阅读&#xff1a;专辑|Linux文章汇总专辑|程序人生专辑|C语言我的知识小密圈关注公众号&#xff0c;后台回复「1024」获取学习资料网盘链接。欢迎点赞&#xff0c;关注&#xff…

matplotlib绘制矢量图像(svg),pdf and ps文件

机器学习的过程中处理数据&#xff0c;会遇到数据可视化的问题&#xff0e; 大部分都是利用python的matplotlib库进行数据的可视化处理&#xff0e; plt.show() 默认都是输出.png文件&#xff0c;图片只要稍微放大一点&#xff0c;就糊的不行&#xff0e; 下面给出一段正常进行…

vue获取div中的值_一篇文章看懂Vue.js的11种传值通信方式

面试的时候,也算是常考的一道题目了,而且,在日常的开发中,对于组件的封装,尤其是在 ui组件库中,会用到很多,下面,就来详细的了解下,通过这篇文章的学习,可以提升项目中组件封装的灵活性,可维护性,话不多说,先从大致的通信方式分类说起,然后依次非常详细地介绍,看…

努力过头了,其实并不好

昨天下午下班的时候&#xff0c;有一个读者朋友找我聊天。我再说下聊天这个事情&#xff0c;如果大家有事情&#xff0c;可以在群里艾特我&#xff0c;因为重要的群我会置顶&#xff0c;置顶的群我会看消息&#xff0c;你在群里艾特我我一定能看到。当然&#xff0c;在群里发一…

Debug Assertion Failed! FIle:afxhtml.inl Line 57的错误的解决

作者&#xff1a;朱金灿 来源&#xff1a;http://blog.csdn.net/clever101/ 同事问我一个奇怪的问题&#xff1a;程序在他的机子上运行的好好的&#xff0c;但是其它人下载了源码之后可以编译通过&#xff0c;但是debug模式下调试运行到应用程序类中的InitInstance函数中的 i…

jstack 脚本 自动日志_GitLab从安装到全自动化备份一条龙

原文地址[1]欢迎star需求1.在新服务器上安装并搭建好gitlab2.手动自动将旧服务器上的gitlab备份3.手动自动将gitlab备份包scp到新服务器上4.手动自动恢复新服务器上的gitlab备份包5.在新旧服务器上自动删除过期备份包前提1.版本•gitlab-ce是社区版•gitlab-ee是企业版1.方案•…

纪念音视频界前辈-雷霄骅

这是一篇记录文&#xff0c;纪念一位在音视频领域研究的博士雷霄骅。雷霄骅生前是中国传媒大学通信与信息系统博士在读生&#xff0c;于2016年7月17日凌晨猝死在学校主楼五层&#xff0c;10月就将迎来自己26岁的生日。雷霄骅在音视频领域有很深的造诣和贡献&#xff0c;指导了很…

引发类型为“System.Windows.Forms.AxHost+InvalidActiveXStateException”的异常 解决

调用ActiveX控件时的异常&#xff0c;引起这个异常的可能性有很多&#xff0c;我的是如果在设计界面拖控件没问题&#xff0c;但是自己new出来就报错。 仔细想了一下&#xff0c;拖控件多了一个add进Form的 操作&#xff0c;把这个操作加上就OK了 AxControl control new AxCon…

[转]版本二写代码的小女孩

天冷极了&#xff0c;下着雪&#xff0c;又快黑了。这是一年的最后一天——大年夜。在这又冷又黑的晚上&#xff0c;一个乖巧的小女孩在机房里调试程序。她从家里出来的时候还穿着一件外套&#xff0c;但是有什么用呢&#xff1f;那是一双很大的外套——那么大&#xff0c;不知…

gcc和g++有什么区别?

来源 | C语言中文网发展至今&#xff0c;GCC 编译器已经更新至 V10 版本&#xff0c;其功能也由最初仅能编译 C 语言&#xff0c;扩增至可以编译多种编程语言&#xff0c;其中就包括 C 。除此之外&#xff0c;当下的 GCC 编译器还支持编译 Go、Objective-C&#xff0c;Objectiv…

druid删除数据_Apache druid 删除数据流程 0.13

背景由于前端时间数据导入出现问题&#xff0c;导致druid 中的数据需要重新导入&#xff0c;但又要防止数据重复,需要把数据彻底清理。问题由于druid 属于时间序列数据库&#xff0c;删除的时候只能时间范围删除。删除流程根据时间范围查询segements 标识(在coordinator节点查询…

使用C语言扩展Python(四)

上一篇里的LAME项目已经展示了python如何与C语言交互&#xff0c;但程序仍不够理想&#xff0c;在python这一端仅仅是传递源文件和目标文件的路径&#xff0c;再调用C模块的encode方法来进行编码&#xff0c;但问题在于你无法控制encode函数&#xff0c;比如你想编码的源文件如…

案例 github_2019年12月Github上最热门的Java开源项目,速来围观!

转眼之间&#xff0c;已经进入了2020年&#xff0c;2019年发生的一切仿佛就在昨天。那么&#xff0c;刚过去不久的12月份GitHub上最热门的Java开源项目排行已经出炉啦。下面我带大家一起来看看上榜详情&#xff1a;1、Alinkhttps://github.com/alibaba/Alink Star 1695Alink 是…

实战CRC校验 | 固件如何校验自身完整性?

来源&#xff1a;公众号【鱼鹰谈单片机】作者&#xff1a;鱼鹰Osprey在一些比较严格的行业里面&#xff0c;不是说你的程序能完成必要功能就可以&#xff0c;还需要添加一些额外的功能&#xff0c;比如最常见的看门狗功能&#xff0c;它可以在程序死机时完成重启&#xff0c;但…

我的职场故事(四)

我高高兴兴到了C公司&#xff0c;第一次发现自己老了。在A公司的时候刚刚毕业&#xff0c;青春无敌。到了B公司&#xff0c;公司都招有经验的&#xff0c;我还算比较年轻。到了C公司&#xff0c;我的小弟们都是后80.我就变的是部门的老同事了。叫我姐的人真的不少&#xff0c;I…

想一个颠覆性技术方向建议,你能想到什么?

如上图&#xff0c;是这次文章的主题。我对这个问题是有想法的&#xff0c;我现在是做音频研究&#xff0c;但是我觉得未来核心的方向一定是能源。试想一下&#xff0c;现在的手机功能越来越多&#xff0c;移动设备将会占领我们未来很长一段时间&#xff0c;那么手机的电池要如…

centos 安装idea 非可视化_太厉害了!目前 Redis 可视化工具最全的横向评测

转自&#xff1a;一入码坑深似海链接&#xff1a;www.jianshu.com/p/cb9f4dcb3b921. 命令行不知道大家在日常操作redis时用什么可视化工具呢&#xff1f;以前总觉得没有什么太好的可视化工具&#xff0c;于是问了一个业内朋友。对方回&#xff1a;你还用可视化工具&#xff1f;…

解决克隆clone github 仓库速度过慢的问题

解决克隆clone GitHub 仓库速度过慢的问题 由于大家都懂的原因&#xff0c;我们访问GitHub的速度确实有点慢&#xff0c;特别是克隆比较大的仓库的时候&#xff0c;那速度简直无法直视。 今天我就给大家带来一个邪门歪道&#xff0c;不通过FQ来解决速度问题。 先说结论吧&#…

ibator1.2.2

编译完了&#xff0c;接下来怎么用&#xff1f;怎么生成的东西还是老样子&#xff1f;别急&#xff0c;看document: Version 1.2.2 iBATIS 3.x Support Ibator now includes support for iBATIS version 3, as well as maintinaing the existing support for iBATIS version 2.…