大话Android中的Handler机制
在Android的线程间通信中,Handler独当一面,无论是framework层还是app层中都出现的相当频繁,有必要好好的拿出来深挖一下它的实现原理。而要说Handler的通信机制,除了Handler外,另外几个重要的类是不得不提的,他们就是Looper、MessageQueue、Message。在仔细研读这几个类的代码后,我试着用一件身边的事情来阐述他们之间的关系。
好的,下面开始我们的角色扮演游戏。Handler饰演寄信的小白,Looper饰演店员B,MessageQueue饰演店员A,Message饰演信。
小白同学逛某宝的时候看到一个店铺,店铺slogen大概是这样——“写封信给N年后的自己”,小白看到这个创意被深深吸引,果断下单,信的标题是写给10年后的自己,内容非常简洁,大致是这样的:当你看到这封信的时候说明咱家还没拆迁,别死等了,好好工作吧(请忽略信内容 -_-)。店铺接单后,店员A不久便制作出了小白的信件然后按照小白要求的10年延迟时间将信件按延迟时间从小到大插入到店里存信件的书架中,同时店员A还需要负责从书架取信(你丫放的信只有你知道怎么取)。而店里还有店员B,他每天的工作内容是问店员A拿第一封信,如果时间已到就把它拿走寄出去。至此,故事剧情结束。
我们把上面的剧情再按程序的逻辑走一遍,看是否有瑕疵。Handler是Message的发起者,它把Message告诉MessageQueue,MessageQueue按执行时间when的大小,将Message插入单向链表中(下单),Looper不断从MessageQueue里取出Message检测执行时间when并执行(取信、寄出)。消息的入口和出口都拎出来了,但是还是有瑕疵:
问题1、Handler是如何与Looper、MessageQueue建立关联的?
当创建Handler对象会传入一个Looper对象或者通过静态方法Looper.myLooper()拿到当前线程的Looper对象及Looper对象中的MessageQueue对象。
问题2、既然是多线程通信,那当Handler在sendMessage的时候,如何知道自己的出生地是哪个线程,换句话说将Message给到哪个线程的MessageQueue?
在问题1中我们已经明确了Handler大部分情况下是通过Looper.myLooper()获得当前线程的Looper对象,拿到了Looper对象自然也就拿到了对应的MessageQueue对象,但是现在问题来了,Looper.myLooper()作为一个静态方法是如何做到返回调用该方法的线程的Looper对象的呢?嘿嘿,下面ThreadLocal<T>隆重登场!Looper类中有一神奇的静态常量ThreadLocal<Looper> sThreadLocal,它能对访问做到线程隔离,即在线程A中get()只能拿到在线程A中set进去的值(关于ThreadLocal<T>这里只讲这么多)。正是基于ThreadLocal这种神奇的实现,使得我们可以通过Looper.myLooper()获得当前线程的Looper对象。贴代码
public final class Looper {
...
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));
}
...
/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
...
}
可以看到,myLooper()返回不为空的前置条件是Looper.prepare()被调用,所以如果要在子线程new一个Handler,切记先在子线程中调用prepare()并且一个线程只能调用一次哦!什么?你问为什么在主线程中不需要调prepare()?主线程当然也要调,只不过主线程在进程启动时系统就帮我们把Looper prepare好了(对启动过程感兴趣的可以去看看android.app.ActivityThread::main(String[])),千万不要再次调用了。
问题3、多个线程都在sendMessage,MessageQueue在插入消息时如何做到线程同步?
这个不太好吹,就是加了同步锁锁住了当前MessageQueue对象呗,详见下方代码
public final class MessageQueue {
....
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
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 {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
....
}
问题4、Looper对象以死循环的方式取Message,会不会导致线程阻塞,主线程这样操作不会ANR吗?当队列中没有Message时,是否仍会造成CPU资源消耗?
既然死循环,那当然会阻塞,而且要的就是阻塞,是不是很惊喜~原因在于main函数作为入口,main函数执行完毕进程就结束了,那为了保持我们的app持续的运行,当然要想办法不让main执行完毕,而loop()方法正好达到了这个目的。至于是否ANR,我们要知道Android的所有生命周期都是通过Handler机制处理的,如果Handler中的某个处理消息的方法耗时过长,就会导致其它消息响应不及时,如果这些消息里有用户的操作类消息或者别的ANR敏感的消息,就可能出现ANR。
关于死循环是否会造成无意义的资源消耗,这个是不会的,这里用到了linux的管道和epoll模型,在取消息的时候通过nativePollOnce(ptr, nextPollTimeoutMillis);阻塞循环,等待nextPollTimeoutMillis后继续取消息(有Message但是when未到达)或阻塞直到管道中有新消息写入(nextPollTimeoutMillis=-1),而在入队的时候检测并唤醒epoll。
了解了Handler机制后,在使用Handler的时候要注意以下问题:
1、初始化Handler前务必调用Looper.prepare(); 主线程中不要画蛇添足去操作Looper;
2、sendMessage前确保Looper.loop()已被调用;
3、利用Message.obtain()的复用机制,避免创建对象的资源消耗。
大话Android中的Handler机制的更多相关文章
- Android中的Handler机制
直接在UI线程中开启子线程来更新TextView显示的内容,运行程序我们会发现,如下错 误:android.view.ViewRoot$CalledFromWrongThreadException: ...
- Android中的Handler的机制与用法详解
概述: 很多android初学者对android 中的handler不是很明白,其实Google参考了Windows的消息处理机制, 在Android系统中实现了一套类似的消息处理机制.在下面介绍ha ...
- Android中利用Handler实现消息的分发机制(三)
在第二篇文章<Android中利用Handler实现消息的分发机制(一)>中,我们讲到主线程的Looper是Android系统在启动App的时候,已经帮我们创建好了,而假设在子线程中须要去 ...
- 转:Android中的Handler的机制与用法详解
注:Message类的用法: message的几个参数都可以携带数据,其中arg1与arg2可以携带int类型,what是用户自定义的int型,这样接受者可以了解这个消息的信息. 说明:使用Messa ...
- Android中使用Handler造成内存泄露的分析和解决
什么是内存泄露?Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用所指向 ...
- Android中使用Handler造成内存泄露
1.什么是内存泄露? Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用 ...
- 浅析Android中的消息机制(转)
原博客地址:http://blog.csdn.net/liuhe688/article/details/6407225 在分析Android消息机制之前,我们先来看一段代码: public class ...
- 浅析Android中的消息机制(转)
在分析Android消息机制之前,我们先来看一段代码: public class MainActivity extends Activity implements View.OnClickListen ...
- 浅析Android中的消息机制-解决:Only the original thread that created a view hierarchy can touch its views.
在分析Android消息机制之前,我们先来看一段代码: public class MainActivity extends Activity implements View.OnClickListen ...
随机推荐
- HDU 5963 朋友 题解
题目 B君在围观一群男生和一群女生玩游戏,具体来说游戏是这样的: 给出一棵n个节点的树,这棵树的每条边有一个权值,这个权值只可能是0或1. 在一局游戏开始时,会确定一个节点作为根.接下来从女生开始,双 ...
- H5+CSS复习笔记(全)
1.自结束标签和注释 通常标签都是成对出现,如<h1></h1>,<div></div>等等.但是又些标签是没有结束标签的,成为自结束标签,如<i ...
- Idea 中 使用 devtools 热部署 spring-boot 应用 无需重新启动
描述: 在我们使用spring-boot开发时,如果在开发者调试项目,边修改边调试运行,如果每次修改 java文件或者配置文件后都需要重新启动程序,如果程序启动比较慢的化,每次修改一点东西都要重新启动 ...
- python 并发 ThreadPoolExecutor
正文:Executor是一个抽象类,子类: ThreadPoolExecutor和ProcessPoolExecutor ,一个线程池,一个进程池. future对象:在未来的某一时刻完成操作的对象. ...
- Python模块04/包/logging日志
Python模块04/包/logging日志 目录 Python模块04/包/logging日志 内容大纲 1.包 2.logging日志 3.今日总结 内容大纲 1.包 2.logging日志 1. ...
- tensorboard学习笔记
TensorBoard 默认是不会记录每个节点的用时.耗费的内存大小等这些信息的,那么如何才能在图上显示这些信息呢?关键就是如下这些代码,主要就是在 sess.run() 中加入 options 和 ...
- 安卓移动端line-height垂直居中出现偏移的解决方法
目前移动端在项目使用的rem,安卓手机上line-height属性,让它的值等于height,结果发现是不居中的. 出现此问题的原因是Android在排版计算的时候参考了primyfont字体的相关属 ...
- 从JIT到类加载再到实现原理解式Lambda编译慢的问题
问题回顾 描述的话不多说,直接上图: 看到输出结果了吗?为什么第一次和第二次的时间相差如此之多?咱们一起琢磨琢磨,也可以先去看看结论再回过头看分析 注:并非仅第二次快,而是除了第一次,之后的每一次都很 ...
- 【真实分享】学习linux!让我工资翻5倍!从月薪3000到年薪18W!只用了六个月!
月薪3000到年薪18W,我用了六个月时间.从只会皮毛,到一家公司的运维工程师主力,我的故事蛮神奇的,今天和大家分享一下我自己的经历. 我今年26岁,之前做个体,修过电脑,卖过电脑,做过桌面运维,一直 ...
- Python 为什么用 # 号作注释符?
关于编程语言中的注释,其重要性基本上已为大家所共识. 然而关于注释的规范,这个话题就像我们之前聊过的缩进.终止符和命名方式一样,众口难调. 注释符通常可分为两种,即行注释与块注释(inline/blo ...