06.Android之消息机制问题
目录介绍
- 6.0.0.1 谈谈消息机制Hander作用?有哪些要素?流程是怎样的?
- 6.0.0.2 为什么一个线程只有一个Looper、只有一个MessageQueue,可以有多个Handler?
- 6.0.0.3 可以在子线程直接new一个Handler吗?会出现什么问题,那该怎么做?
- 6.0.0.4 Looper.prepare()能否调用两次或者多次,会出现什么情况?
- 6.0.0.5 为什么系统不建议在子线程访问UI,不对UI控件的访问加上锁机制的原因?
- 6.0.0.6 如何获取当前线程的Looper?是怎么实现的?(理解ThreadLocal)
- 6.0.0.7 Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?
- 6.0.0.8 Handler.sendMessageDelayed()怎么实现延迟的?结合Looper.loop()循环中,Message=messageQueue.next()和MessageQueue.enqueueMessage()分析。
- 6.0.0.9 Message可以如何创建?哪种效果更好,为什么?
- 6.0.1.3 使用Hanlder的postDealy()后消息队列会发生什么变化?
- 6.0.1.4 ThreadLocal有什么作用?
好消息
- 博客笔记大汇总【15年10月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计500篇[近100万字],将会陆续发表到网上,转载请注明出处,谢谢!
- 链接地址:https://github.com/yangchong211/YCBlogs
- 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!所有的笔记将会更新到GitHub上,同时保持更新,欢迎同行提出或者push不同的看法或者笔记!
6.0.0.1 谈谈消息机制Hander作用?有哪些要素?流程是怎样的?
- 作用:
- 跨线程通信。当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。
- 四要素:
- Message(消息):需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理。技术博客大总结
- MessageQueue(消息队列):用来存放Handler发送过来的消息,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
- Handler(处理者):负责Message的发送及处理。通过 Handler.sendMessage() 向消息池发送各种消息事件;通过 Handler.handleMessage() 处理相应的消息事件。
- Looper(消息泵):通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者。
- 具体流程
- Handler.sendMessage()发送消息时,会通过MessageQueue.enqueueMessage()向MessageQueue中添加一条消息;
- 通过Looper.loop()开启循环后,不断轮询调用MessageQueue.next();
- 调用目标Handler.dispatchMessage()去传递消息,目标Handler收到消息后调用Handler.handlerMessage()处理消息。
6.0.0.2 为什么一个线程只有一个Looper、只有一个MessageQueue,可以有多个Handler?
- 注意:一个Thread只能有一个Looper,可以有多个Handler
- Looper有一个MessageQueue,可以处理来自多个Handler的Message;MessageQueue有一组待处理的Message,这些Message可来自不同的Handler;Message中记录了负责发送和处理消息的Handler;Handler中有Looper和MessageQueue。
- 为什么一个线程只有一个Looper?技术博客大总结
- 需使用Looper的prepare方法,Looper.prepare()。可以看下源代码,Android中一个线程最多仅仅能有一个Looper,若在已有Looper的线程中调用Looper.prepare()会抛出RuntimeException(“Only one Looper may be created per thread”)。
- 所以一个线程只有一个Looper,不知道这样解释是否合理!更多可以查看我的博客汇总:https://github.com/yangchong211/YCBlogs
public static void prepare() {
prepare(true);
} private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
6.0.0.3 可以在子线程直接new一个Handler吗?会出现什么问题,那该怎么做?
- 不同于主线程直接new一个Handler,由于子线程的Looper需要手动去创建,在创建Handler时需要多一些方法:
- Handler的工作是依赖于Looper的,而Looper(与消息队列)又是属于某一个线程(ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程则无法获取到),其他线程不能访问。因此Handler就是间接跟线程是绑定在一起了。因此要使用Handler必须要保证Handler所创建的线程中有Looper对象并且启动循环。因为子线程中默认是没有Looper的,所以会报错。
- 正确的使用方法是:技术博客大总结
handler = null;
new Thread(new Runnable() {
private Looper mLooper;
@Override
public void run() {
//必须调用Looper的prepare方法为当前线程创建一个Looper对象,然后启动循环
//prepare方法中实质是给ThreadLocal对象创建了一个Looper对象
//如果当前线程已经创建过Looper对象了,那么会报错
Looper.prepare();
handler = new Handler();
//获取Looper对象
mLooper = Looper.myLooper();
//启动消息循环
Looper.loop();
//在适当的时候退出Looper的消息循环,防止内存泄漏
mLooper.quit();
}
}).start();
- 主线程中默认是创建了Looper并且启动了消息的循环的,因此不会报错:应用程序的入口是ActivityThread的main方法,在这个方法里面会创建Looper,并且执行Looper的loop方法来启动消息的循环,使得应用程序一直运行。
6.0.0.4 Looper.prepare()能否调用两次或者多次,会出现什么情况?
- Looper.prepare()方法源码分析
- 可以看到Looper中有一个ThreadLocal成员变量,熟悉JDK的同学应该知道,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
public static void prepare() {
prepare(true);
} private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
- 思考:Looper.prepare()能否调用两次或者多次
- 如果运行,则会报错,并提示prepare中的Excetion信息。由此可以得出在每个线程中Looper.prepare()能且只能调用一次
- 技术博客大总结
//这里Looper.prepare()方法调用了两次
Looper.prepare();
Looper.prepare();
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
Log.i(TAG, "在子线程中定义Handler,并接收到消息。。。");
}
}
};
Looper.loop();
6.0.0.5 为什么系统不建议在子线程访问UI,不对UI控件的访问加上锁机制的原因?
- 为什么系统不建议在子线程访问UI
- 系统不建议在子线程访问UI的原因是,UI控件非线程安全,在多线程中并发访问可能会导致UI控件处于不可预期的状态。
- 不对UI控件的访问加上锁机制的原因
- 上锁会让UI控件变得复杂和低效
- 上锁后会阻塞某些进程的执行技术博客大总结
6.0.0.7 Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?
- 问题描述
- 在处理消息的时候使用了Looper.loop()方法,并且在该方法中进入了一个死循环,同时Looper.loop()方法是在主线程中调用的,那么为什么没有造成阻塞呢?
- ActivityThread中main方法
- ActivityThread类的注释上可以知道这个类管理着我们平常所说的主线程(UI线程)
- 首先 ActivityThread 并不是一个 Thread,就只是一个 final 类而已。我们常说的主线程就是从这个类的 main 方法开始,main 方法很简短
public static final void main(String[] args) {
...
//创建Looper和MessageQueue
Looper.prepareMainLooper();
...
//轮询器开始轮询
Looper.loop();
...
}
- ActivityThread类的注释上可以知道这个类管理着我们平常所说的主线程(UI线程)
- Looper.loop()方法无限循环
- 看看Looper.loop()方法无限循环部分的代码
while (true) {
//取出消息队列的消息,可能会阻塞
Message msg = queue.next(); // might block
...
//解析消息,分发消息
msg.target.dispatchMessage(msg);
...
}
- 看看Looper.loop()方法无限循环部分的代码
- 为什么这个死循环不会造成ANR异常呢?
- 因为Android 的是由事件驱动的,looper.loop() 不断地接收事件、处理事件,每一个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下,如果它停止了,应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。技术博客大总结
- 处理消息handleMessage方法
- 如下所示
- 可以看见Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施。
- 如果某个消息处理时间过长,比如你在onCreate(),onResume()里面处理耗时操作,那么下一次的消息比如用户的点击事件不能处理了,整个循环就会产生卡顿,时间一长就成了ANR。
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
break;
case RELAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");
ActivityClientRecord r = (ActivityClientRecord) msg.obj;
handleRelaunchActivity(r);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
break;
case PAUSE_ACTIVITY:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
handlePauseActivity((IBinder) msg.obj, false, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 2) != 0);
maybeSnapshot();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case PAUSE_ACTIVITY_FINISHING:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
handlePauseActivity((IBinder) msg.obj, true, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 1) != 0);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
...........
}
}
- 如下所示
- loop的循环消耗性能吗?
- 主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。
- 简单的来说:ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。
6.0.0.9 Message可以如何创建?哪种效果更好,为什么?runOnUiThread如何实现子线程更新UI?
- 创建Message对象的几种方式:技术博客大总结
- Message msg = new Message();
- Message msg = Message.obtain();
- Message msg = handler1.obtainMessage();
- 后两种方法都是从整个Messge池中返回一个新的Message实例,能有效避免重复Message创建对象,因此更鼓励这种方式创建Message
- runOnUiThread如何实现子线程更新UI
- 看看源码,如下所示
- 如果msg.callback为空的话,会直接调用我们的mCallback.handleMessage(msg),即handler的handlerMessage方法。由于Handler对象是在主线程中创建的,所以handler的handlerMessage方法的执行也会在主线程中。
- 在runOnUiThread程序首先会判断当前线程是否是UI线程,如果是就直接运行,如果不是则post,这时其实质还是使用的Handler机制来处理线程与UI通讯。
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
} @Override
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
6.0.1.3 使用Hanlder的postDealy()后消息队列会发生什么变化?
- post delay的Message并不是先等待一定时间再放入到MessageQueue中,而是直接进入并阻塞当前线程,然后将其delay的时间和队头的进行比较,按照触发时间进行排序,如果触发时间更近则放入队头,保证队头的时间最小、队尾的时间最大。此时,如果队头的Message正是被delay的,则将当前线程堵塞一段时间,直到等待足够时间再唤醒执行该Message,否则唤醒后直接执行。
6.0.1.4 ThreadLocal有什么作用?
- 线程本地存储的功能
- ThreadLocal类可实现线程本地存储的功能,把共享数据的可见范围限制在同一个线程之内,无须同步就能保证线程之间不出现数据争用的问题,这里可理解为ThreadLocal帮助Handler找到本线程的Looper。
- 技术博客大总结
- 怎么存储呢?底层数据结构是啥?
- 每个线程的Thread对象中都有一个ThreadLocalMap对象,它存储了一组以ThreadLocal.threadLocalHashCode为key、以本地线程变量为value的键值对,而ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,也就包含了一个独一无二的threadLocalHashCode值,通过这个值就可以在线程键值值对中找回对应的本地线程变量。
关于其他内容介绍
01.关于博客汇总链接
02.关于我的博客
- 我的个人站点:www.yczbj.org, www.ycbjie.cn
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/yczbj/activities
- 简书:http://www.jianshu.com/u/b7b2c6ed9284
- csdn:http://my.csdn.net/m0_37700275
- 喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/
- 开源中国:https://my.oschina.net/zbj1618/blog
- 泡在网上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 邮箱:yangchong211@163.com
- 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault头条:https://segmentfault.com/u/xiangjianyu/articles
- 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e
06.Android之消息机制问题的更多相关文章
- Android的消息机制简单总结
参考文章: http://gityuan.com/2015/12/26/handler-message-framework/#next 参考资料: Android Framework的源码: Mess ...
- 《Android开发艺术探索》读书笔记 (10) 第10章 Android的消息机制
第10章 Android的消息机制 10.1 Android消息机制概述 (1)Android的消息机制主要是指Handler的运行机制,其底层需要MessageQueue和Looper的支撑.Mes ...
- Android的消息机制
一.简介 ①.我们不能在子线程中去访问UI空控件,这是时候只能通过Handler将更新UI的操作放到主线程中去执行 ②.Handler的组成:messageQueue和Looper的支持 ③.Mess ...
- 【原创】源码角度分析Android的消息机制系列(一)——Android消息机制概述
ι 版权声明:本文为博主原创文章,未经博主允许不得转载. 1.为什么需要Android的消息机制 因为Android系统不允许在子线程中去访问UI,即Android系统不允许在子线程中更新UI. 为什 ...
- 【原创】源码角度分析Android的消息机制系列(五)——Looper的工作原理
ι 版权声明:本文为博主原创文章,未经博主允许不得转载. Looper在Android的消息机制中就是用来进行消息循环的.它会不停地循环,去MessageQueue中查看是否有新消息,如果有消息就立刻 ...
- 【原创】源码角度分析Android的消息机制系列(二)——ThreadLocal的工作过程
ι 版权声明:本文为博主原创文章,未经博主允许不得转载. 在上一篇文章中,我们已经提到了ThreadLocal,它并非线程,而是在线程中存储数据用的.数据存储以后,只能在指定的线程中获取到数据,对于其 ...
- Android 基础 十一 Android的消息机制
Handler是Android消息机制的上层接口,这使得在开发应用过程中我们只需要和Handler交互即可.Handler的使用过程很简单,通过它可以轻松地将一个任务切换到Handler所在的线程中去 ...
- 聊一聊Android的消息机制
聊一聊Android的消息机制 侯 亮 1概述 在Android平台上,主要用到两种通信机制,即Binder机制和消息机制,前者用于跨进程通信,后者用于进程内部通信. 从技术实现上来说,消息机制还是比 ...
- Android开发——Android的消息机制详解
)子线程默认是没有Looper的,Handler创建前,必须手动创建,否则会报错.通过Looper.prepare()即可为当前线程创建一个Looper,并通过Looper.loop()来开启消息循环 ...
- Android之消息机制Handler,Looper,Message解析
PS:由于感冒原因,本篇写的有点没有主干,大家凑合看吧.. 学习内容: 1.MessageQueue,Looper,MessageQueue的作用. 2.子线程向主线程中发送消息 3.主线程向子线程中 ...
随机推荐
- 《ASP.ENT Core 与 RESTful API 开发实战》-- (第5章)-- 读书笔记(下)
第 5 章 使用 Entity Framework Core 5.4 重构 Controller 和 Action 重构 AuthorController 构造函数重构 public IMapper ...
- Widget模式
Widget模式 Widget模式是指借用Web Widget思想将页面分解成组件,针对部件开发,最终组合成完整的页面,Web Widget指的是一块可以在任意页面中执行的代码块,Widget模式不属 ...
- SpringBoot+Shiro+LayUI权限管理系统项目-9.核心知识点总结
1.说明 本篇讲一下本项目几个重要的知识点,详细看源码,文章下方捐赠或QQ联系捐赠获取. 2.Shiro如何设置密码加密算法 1.在shiro配置文件中添加: @Bean public HashedC ...
- 树莓派/Linux ubuntu 开机自动改网络mac地址(主要适用于拷贝内存卡的情况/不同树莓派mac地址不同)
树莓派/Linux ubuntu 开机自动改网络mac地址(主要适用于拷贝内存卡的情况/不同树莓派mac地址不同) yaml文件名根据自己原卡中名字更改 address=$(cat /sys/clas ...
- python 创建动态类
一般情况下多数是预先定义类 而少数特殊情况就需要去动态创建类了,直接贴代码. class BaseModel(Model): class Meta: database = _tb class_new ...
- win32 - 使用GDI+播放gif图片
今天做case的时候遇到一个这样的问题,故记录下来. Codeproject有类似的案例,不过是使用的MFC模板编译的. 因为我们只需要win32程序,所以就....代码如下: CodeProject ...
- MYSQL查询数据表中某个字段包含某个数值
当某个字段中字符串是"1,2,3,4,5,6"或者"123456"查询数据表中某个字段是否包含某个值1:模糊查询 使用like select * ...
- React 组件通信方式
人生的游戏不在于拿了一副好牌,而在于怎样去打好坏牌,世上没有常胜将军,勇于超越自我者才能得到最后的奖杯. 1. 父子组件通信方式 1.1 父组件传递到子组件 直接通过属性进行传递,数据的传递可以提高组 ...
- Python函数每日一讲 - 一文让你彻底掌握Python中的frozenset函数
引言 在 Python 中,frozenset() 函数是一个重要的工具,用于创建不可变的集合对象.本文将介绍 frozenset() 函数的语法.用法示例以及实际应用场景,帮助大家更好地理解和应用这 ...
- HTTP1.0/HTTP1.1/HTTP2.0的演进
HTTP1.0 短连接,每次请求都需要重新建立连接 不支持断点续传 HTTP1.1 支持长连接,同一个客户端连接可保持长连接,请求可在连接中顺序发出. 查看http请求头中有keepalive 参数 ...