在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机制的更多相关文章

  1. Android中的Handler机制

    直接在UI线程中开启子线程来更新TextView显示的内容,运行程序我们会发现,如下错 误:android.view.ViewRoot$CalledFromWrongThreadException: ...

  2. Android中的Handler的机制与用法详解

    概述: 很多android初学者对android 中的handler不是很明白,其实Google参考了Windows的消息处理机制, 在Android系统中实现了一套类似的消息处理机制.在下面介绍ha ...

  3. Android中利用Handler实现消息的分发机制(三)

    在第二篇文章<Android中利用Handler实现消息的分发机制(一)>中,我们讲到主线程的Looper是Android系统在启动App的时候,已经帮我们创建好了,而假设在子线程中须要去 ...

  4. 转:Android中的Handler的机制与用法详解

    注:Message类的用法: message的几个参数都可以携带数据,其中arg1与arg2可以携带int类型,what是用户自定义的int型,这样接受者可以了解这个消息的信息. 说明:使用Messa ...

  5. Android中使用Handler造成内存泄露的分析和解决

    什么是内存泄露?Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用所指向 ...

  6. Android中使用Handler造成内存泄露

    1.什么是内存泄露? Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用 ...

  7. 浅析Android中的消息机制(转)

    原博客地址:http://blog.csdn.net/liuhe688/article/details/6407225 在分析Android消息机制之前,我们先来看一段代码: public class ...

  8. 浅析Android中的消息机制(转)

    在分析Android消息机制之前,我们先来看一段代码: public class MainActivity extends Activity implements View.OnClickListen ...

  9. 浅析Android中的消息机制-解决:Only the original thread that created a view hierarchy can touch its views.

    在分析Android消息机制之前,我们先来看一段代码: public class MainActivity extends Activity implements View.OnClickListen ...

随机推荐

  1. 线性dp打鼹鼠

    题目大意 鼹鼠是一种很喜欢挖洞的动物,但每过一定的时间,它还是喜欢把头探出到地面上来透透气的.根据这个特点阿Q编写了一个打鼹鼠的游戏:在一个 的网格中,在某些时刻鼹鼠会在某一个网格探出头来透透气.你可 ...

  2. Maven一键部署Springboot到Docker仓库,为自动化做准备

    1 前言 前面<Springboot整合MongoDB的Docker开发,其它应用也类似>讲解了如何做Docker开发.如何把Springboot应用打包成一个镜像,但它是手动的,本文将讲 ...

  3. js代码段

    1.数组去重 Array.prototype.DuplicateRemoval = function(){ let res = [this[0]]; for(let i = 1; i < thi ...

  4. EM算法理论与推导

    EM算法(Expectation-maximization),又称最大期望算法,是一种迭代算法,用于含有隐变量的概率模型参数的极大似然估计(或极大后验概率估计) 从定义可知,该算法是用来估计参数的,这 ...

  5. 《利用Python进行数据分析》自学知识图谱-导航

    项目简介 Project Brief <利用Python进行数据分析-第二版>自学过程中整理的知识图谱. Python for Data Analysis: Data Wrangling ...

  6. 阅读手札 | 手把手带你探索『图解 HTTP』

    前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 Github 博客 学习清单: 一.网络基础 TCP/IP 通常使用的网络(包括互联网)是在 TCP/IP 协议族的基础上 ...

  7. javascript : 找到一个树型数据的一个节点及其所有父节点

    如题. (function () { let tree = { "id": 0, "label": "all", "childre ...

  8. javascript : splice(0) 的妙用

    今天看前同事的代码,有个写法很有意思: splice(0). 有意思的是,这并不是一个“合法”的写法. w3school是这样写的: arrayObject.splice(index,howmany, ...

  9. p72_电子邮件

    一.电子邮件格式 信封 abcd@xx.com 内容 2.1 首部(To, Subject)-> (From,To,Subject,Date) 2.2 主体 二.电子邮件组成结构 三.SMTP ...

  10. 利用华为eNSP模拟器实现vlan之间的通信

    eNSP交换机配置VLAN 1. 搭建网络拓扑结构 运行eNSP>新建拓扑>搭建如下图的拓扑结构>启动设备.利用调色板将划分的vlan进行区分. 2. pc机IP地址配置 pc1的I ...