大话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 ...
随机推荐
- centos7-修改默认python为3
安装必要工具 yum-utils: $ sudo yum install yum-utils 使用yum-builddep为Python3构建环境,安装缺失的软件依赖,使用下面的命令会自动处理.$ s ...
- 一个简单的webAPI调用
1.新建一个ASP.NET Web应用程序. 2.选择空模板,WebAPI. 3.在Models文件夹添加Product类. 4.添加空控制器ProductController. 5.ProductC ...
- 你是如何理解Vue的响应式系统的
1.响应式系统简述: 任何一个 Vue Component 都有一个与之对应的 Watcher 实例. Vue 的 data 上的属性会被添加 getter 和 setter 属性. 当 Vue Co ...
- Maven 专题(七):常用命令
mvn archetype:generate : 反向生成项目的骨架 mvn clean: 清除各个模块target目录及里面的内容 mvn compile: 静态编译,根据xx.java生成xx.c ...
- SQLAlchemy(三):外键、连表关系
SQLAlchemy03 /外键.连表关系 目录 SQLAlchemy03 /外键.连表关系 1.外键 2.ORM关系以及一对多 3.一对一的关系 4.多对多的关系 5.ORM层面的删除数据 6.OR ...
- HotSpot的对象模型(6)
接着上一篇,我们继续来讲oopDesc相关的子类. 3.instanceOopDesc类 instanceOopDesc类的实例表示除数组对象外的其它对象.在HotSpot中,对象在内存中存储的布局可 ...
- bzoj2456mode
bzoj2456mode 题意: 给你一个n个数的数列,求出现次数超过n div 2的数(只有1个). 题解: 注意空间只有1M,显然不能开数组.用两个变量,一个存“当前数”,另一个存“当前数”的个数 ...
- bzoj4512[Usaco2016 Jan] Build Gates
bzoj4512[Usaco2016 Jan] Build Gates 题意: 某人从农场的(0,0)出发,沿边界到处乱走,走过的地方会留下栅栏,等走完后问要在多少个栅栏上开门才能使整个农场连通,最多 ...
- js 左右切换 局部刷新
//刷新地方的ID,后面ID前必须加空格 $("#gwc").load(location.href + " #gwc");
- Flink之对时间的处理
window+trigger+watermark处理全局乱序数据,指定窗口上的allowedLateness可以处理特定窗口操作的局部事件时间乱序数据 1.流处理系统中的微批 Flink内部也使用了某 ...