我要做 Android 之消息机制
Android的消息机制指的是Handler的运行机制,本篇将总结Handler机制的相关知识点:
- 消息机制概述
- 消息机制分析
1.消息机制概述
a.作用:跨线程通信。
b.常用场景:当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。
系统不建议在子线程访问UI的原因:UI控件非线程安全,在多线程中并发访问可能会导致UI控件处于不可预期的状态。而不对UI控件的访问加上锁机制的原因有:
- 上锁会让UI控件变得复杂和低效
- 上锁后会阻塞某些进程的执行
c.四要素:
- Message(消息):需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理。
- MessageQueue(消息队列):用来存放Handler发送过来的消息,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
- Handler(处理者)
:负责Message的发送及处理。- Handler.sendMessage():向消息池发送各种消息事件。
- Handler.handleMessage() :处理相应的消息事件。
- Looper(消息泵):通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者。
Thread(线程):负责调度整个消息循环,即消息循环的执行场所。
存在关系:
- 一个Thread只能有一个Looper,可以有多个Handler;
- Looper有一个MessageQueue,可以处理来自多个Handler的Message;
- MessageQueue有一组待处理的Message,这些Message可来自不同的Handler;
- Message中记录了负责发送和处理消息的Handler;
- Handler中有Looper和MessageQueue;
d.实现方法:
- 在主线程实例化一个全局的Handler对象;
- 在需要执行UI操作的子线程里实例化一个Message并填充必要数据,调用Handler.sendMessage(Message msg)方法发送出去;
- 复写handleMessage()方法,对不同Message执行相关操作;
2.消息机制分析
a.工作流程:
- Handler.sendMessage()发送消息时,会通过MessageQueue.enqueueMessage()向MessageQueue中添加一条消息;
- 通过Looper.loop()开启循环后,不断轮询调用MessageQueue.next();
- 调用目标Handler.dispatchMessage()去传递消息,目标Handler收到消息后调用Handler.handlerMessage()处理消息。
简单来看,即Handler将Message发送到Looper的成员变量MessageQueue中,之后Looper不断循环遍历MessageQueue从中读取Message,最终回调给Handler处理。如图:
b.工作原理:
- (1)Looper的创建:先从应用程序的入口函数ActivityThread.main()看起,在这里(主线程)系统自动创建了Looper,主要方法:
//主线程中不需要自己创建Looper public static void main(String[] args) { ...... Looper.prepareMainLooper();//为主线程创建Looper,该方法内部又调用 Looper.prepare() ...... Looper.loop();//开启消息轮询 ...... }
注意:
- 子线程的Looper需要手动去创建,标准写法是:
//子线程中需要自己创建一个Looper new Thread(new Runnable() { @Override public void run() { Looper.prepare();//为子线程创建Looper Looper.loop(); //开启消息轮询 } }).start();
无论是主线程还是子线程,Looper只能被创建一次,即一个Thread只有一个Looper。
所创建的Looper会保存在ThreadLocal(线程本地存储区)中,它不是线程,作用是帮助Handler获得当前线程的Looper。更多讲解见ThreadLocal详解
(2)MessageQueue的创建:在Looper的构造函数创建了一个MessageQueue:
private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
(3)Message轮询及处理:有了Looper和MessageQueue之后通过Looper.loop()开启消息轮询:
public static void loop() { ...... for (;;) {//死循环 Message msg = queue.next(); //用于提取下一条信息,该方法里同样有个for(;;)死循环,当没有可处理该Message的Handler时,会一直阻塞 if (msg == null) { return; } ...... try { msg.target.dispatchMessage(msg);//如果从MessageQueue中拿到Message,由和它绑定的Handler(msg.target)将它发送到MessageQueue end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); } finally { if (traceTag != 0) { Trace.traceEnd(traceTag); } } ...... }
现在就剩创建Handler及Message发送了。(4)先看Handler的创建:有两种形式的Handler:
//第一种:send方式的Handler创建 Handler handler = new Handler() { @Override public void handleMessage(Message msg) { //如UI操作 } }; //第二种:post方式的Handler创建 Handler handler = new Handler();
- (5) Message的发送:
对于send方式的Handler:创建好一个Message后,调用Handler的以下几种常见的方法来发送消息:
sendEmptyMessage(); //发送空消息 sendEmptyMessageAtTime(); //发送按照指定时间处理的空消息 sendEmptyMessageDelayed(); //发送延迟指定时间处理的空消息 sendMessage(); //发送一条消息 sendMessageAtTime(); //发送按照指定时间处理的消息 sendMessageDelayed(); //发送延迟指定时间处理的消息 sendMessageAtFrontOfQueue(); //将消息发送到消息队头
对于post方式的Handler,可在子线程直接调用Handler的以下几种常见方法,使得切换到主线程:
post(Runnable r) postAtFrontOfQueue(Runnable r) postAtTime(Runnable r, Object token, long uptimeMillis) postAtTime(Runnable r, long uptimeMillis) postDelayed(Runnable r, long delayMillis) //例如,postDelayed()方法 handler.postDelayed(new Runnable() { @Override public void run() { //如UI操作 } },300);
通过以上各种Handler的发送方法,都会依次调用 Handler.sendMessageDelayed->Handler.sendMessageAtTime()->MessageQueue.enqueueMessage() 最终将Message发送到MessageQueue。
Q:Message可以如何创建?哪种效果更好,为什么?
创建Message对象的时候,有三种方式,分别为:
1.Message msg = new Message();
2.Message msg2 = Message.obtain();
3.Message msg1 = handler1.obtainMessage();
这三种方式有什么区别呢?
Message msg = new Message();这种就是直接初始化一个Message对象,没有什么特别的。
2)Message msg2 = Message.obtain();
/** * Return a new Message instance from the global pool. Allows us to * avoid allocating new objects in many cases. */ public static Message obtain() { synchronized (sPoolSync) { if (sPool != null) { Message m = sPool; sPool = m.next; m.next = null; m.flags = 0; // clear in-use flag sPoolSize--; return m; } } return new Message(); }
从注释可以得知,从整个Messge池中返回一个新的Message实例,通过obtainMessage能避免重复Message创建对象。
Message msg1 = handler1.obtainMessage();
public final Message obtainMessage() { return Message.obtain(this); }1234 public static Message obtain(Handler h) { Message m = obtain(); m.target = h; return m; } public static Message obtain() { synchronized (sPoolSync) { if (sPool != null) { Message m = sPool; sPool = m.next; m.next = null; m.flags = 0; // clear in-use flag sPoolSize--; return m; } } return new Message(); }
可以看到,第二种跟第三种其实是一样的,都可以避免重复创建Message对象,所以建议用第二种或者第三种任何一个创建Message对象。
Q:主线程中Looper的轮询死循环为何没有阻塞主线程?
正如我们所知,在android中如果主线程中进行耗时操作会引发ANR(Application Not Responding)异常。
造成ANR的原因一般有两种:
- 当前的事件没有机会得到处理(即主线程正在处理前一个事件,没有及时的完成或者looper被某种原因阻塞住了)
- 当前的事件正在处理,但没有及时完成
为了避免ANR异常,android使用了Handler消息处理机制。让耗时操作在子线程运行。
因此产生了一个问题,主线程中的Looper.loop()一直无限循环检测消息队列中是否有新消息为什么不会造成ANR?
while (true) { //取出消息队列的消息,可能会阻塞 Message msg = queue.next(); // might block ... //解析消息,分发消息 msg.target.dispatchMessage(msg); ... }
显而易见的,如果main方法中没有looper进行循环,那么主线程一运行完毕就会退出。
总结:ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的应用也就退出了。
因为Android 的是由事件驱动的,looper.loop() 不断地接收事件、处理事件,每一个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下,如果它停止了,应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。
也就说我们的代码其实就是在这个循环里面去执行的,当然不会阻塞了。
如果某个消息处理时间过长,比如你在onCreate(),onResume()里面处理耗时操作,那么下一次的消息比如用户的点击事件不能处理了,整个循环就会产生卡顿,时间一长就成了ANR。
而且主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。
总结:Looer.loop()方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生ANR异常。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Android之路 - 冷启动解决方案:实现秒开
前言 关于 splash 页面相信每个Android开发者都是非常熟悉的,而且很多人也遇到过需要在splash加个广告图片,然后延迟3秒在进入主页面,splash 应该只是一个启动页面,不应该放广告,但是那又能怎么样呢?又敌不过产品经理。 大多数情况下都会碰到启动白屏和黑屏的情况,那么本文将探讨几种我在开发中用到的几种解决方案。 原理解析 冷启动 什么是冷启动 Android中的冷启动,使用直白的话就是: 当手机 重启 ,点击桌面图标启动应用的过程就是冷启动 未启动手机,长时 未使用,应用被 kill 后,此时点击桌面图标启动应用的过程 冷启动的表现形式 未做处理的情况 点击桌面图标后没有反应,没有瞬间打开应用,也就是没有马上看到应用打开 点击桌面图标后会显示 黑屏 或者 白屏 , 没有及时渲染出页面元素 详情可以查看下图: 冷启动场景演示 从上图可以看出,点击图标后出现了短暂的白屏,然后才显示了 splash 页面的内容,在splash页面进行了延迟 1500毫秒再跳转到主页面。虽然白屏的时间很短暂,但给用户的体验感就不是很好了。 冷启动产生的原因 冷启动产生的主要原因要从APP的启...
- 下一篇
Android 开发中的代码片段(3)地图操作相关
前言 收集常用的代码块,留存记录。此次代码块包含:唤起高德地图导航、唤起百度地图导航 代码 判断手机内安装的地图 /**判断是否安装目标应用*/ private boolean isInstallByread(String packageName) { return new File("/data/data/" + packageName) .exists(); } /** * 判断和打开地图 */ public void navigationMap() { //1.两个地图都安装了,让用户选择 boolean installBaidu = isInstallByread("com.baidu.BaiduMap"); boolean installAmap = isInstallByread("com.autonavi.minimap"); if (installBaidu && installAmap) {//两个地图都安装了 让用户进行选择 showSelectMap(); } else if (installBaidu) {//安装了百度地图 startBaidu...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Mario游戏-低调大师作品
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8编译安装MySQL8.0.19
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7