浅谈Handler机制

简介:
异步消息处理-Handler机制是专门用来线程之间通讯用的.那到底是怎么实现这个线程通信的呢?接下来我们就看看这个Handler的工作原理.
每个线程中只能有一个Handler对象.每个Handler对象创建的时候.都会从Looper的静态方法中.获取这个线程的Looper对象.所以在创建Handler线程之前.一定要先创建Looper的对象.不然会报错.但是程序开始的时候已经帮我们新建好一个主程序的Looper对象了.所以在主线程可以直接新建一个Handler对象

工作原理:
在Handler(异步消息处理)中.其实是由4个部分组成的.每个部分都会与自己的任务和职责.介绍一下四个部分
1.handler
介绍:handler这个对象.翻译过来就是处理者的意思.它主要是负责发送消息.和接收处理消息的.
       但是hanlder离不开Looper和MessageQueue支撑的.而Looper类中又包含着MessageQueue对象.所以创建Handler之前是需要首先创建Looper对象的.但是为什么主线程中又可以直接创建Handler的对象呢.那是在程序一开始.已经为你准备好了Looper的对象了.

那Handler又是怎么跟当前线程的轮询器和消息队列关联上的呢..通过构造函数里面.会通过Looper的MyLooper()方法.来获得绑定当前线程的Looper.

--Looper,MessageQueue在下面的细节中会给你讲解

#下面是解答一下他是怎么把消息发送到信息队列里面的.还有怎么从信息列表里面拿出Msg对象.最后发送(调用)到我们重写的handleMessage()方法中的.

1.发送消息
sendMessage(Message);
解析sendMessage(Message)源码.
首先我们要知道.MessageQueue中的队列并不是集合.而是一个链式的结构
发送的Message的源码很简单. 首先我们肯定要获取消息队列.消息队列就存在Looper中.那我们这个Looper其实就是在新建Handler时候获取的.
当我们获取消息队列之后.就拿到里面的一个成员Message对象.然后会判断当前对象是否为空.
1.为空.
证明列表中没有消息.我们就把这参数里面的Message对象赋值到成员mMessage中

2.不为空.
意味着当前列表存在消息.那我们通过while.找到了当前列表的末端.并且把参数的Msg对象添加的链表队列的末端中

2.接收消息
handleMessage(Message)
介绍:这是一个空方法,为什么呢,因为消息的最终回调是由我们控制的,我们在创建handler的时候都是复写handleMessage方法,然后根据msg.what进行消息识别处理。
解析handleMessage(Message).究竟Looper是怎么调用这个方法的.我们看看Looper中的源码.
Looper中有一个静态loop()方法.这个方法就是首先他会获取这个线程中的Looper对象.
通过MessageQueue对象中的next().方法.获取到当前队列中的Msg.然后经过判断.最后调用Msg.target.dispathMessage(Msg)的方法.(这里的target字段就是一个Handler对象.是当初发送他出去那个.)把Msg放进参数里面

我们在看看dispatchMessage()的源码
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
我们直接看下面的handleMessage(msg).没错.经过一轮的等待.当轮到Msg自己的自己.自己就拿出了当初发送的Handler对象.再调用里面的dispatchMessage.重新回调到我们重写的 dispatchMessage方法中.
最后放一个图


简单小结一下步骤.
1. 添加到消息队列
  当我们调用handler.sendMessage(Msg) -->调用当前线程的Looper中的MessageQueue对象的.enqueueMessage(Msg)方法.正式把Msg对象放到信息列表中

2.Looper轮询.
   系统会在handler对象所在的线程自动调用loop()方法.开启Looper的消息轮询.Looper就会不断从MessageQueue中循环取出Msg.若当前列表没有消息.则进入休眠(Linux底层实现)状态.然后利用Msg对象自身携带的Handler对象.调用handleMessage(msg)方法

2.Message(信息)
介绍:Message对象.跟名字一样.是一个信息对象.里面能携带一个Object对象和arg1,arg2,两个整形数据.当然还有表名身份的标识.what字段
还能通过SetData()去携带复杂数据(Bundle).

携带的信息(字段):
1.Object obj;//相当于能携带任务对象过去.
2.int arg1;
3.int arg2;
4.int what; //为这个数据加一个标识.当我们接受信息的时候就可以通过这个what字段来筛选信息了
5.还可以通过setData(Bundle)  发送一个Bundle对象过去.

无论我们发送消息还是接收消息.只需要我们把里面携带字段填充数据.就可以了~甚至不填充.发一个空的Message.让接收器那边接收该信号也可以.使用的条件都是比较自由的.根据需求来定义吧!

优化:
谷歌为了避免msg的大量创建.所以建立了一个msg的对象池.当msg被轮询器获得的时候.msg对象就会被轮询器放置到对象池中.为我们提供复用.所以我们新建Message对象的时候.我们可以直接从线程池中获取.通过以下两个方法来获取.
1.Handler
Handler.obtainMessage().

2.Message
Message.obtain()


#当池中没有对象的时候也不用担心我们会获取一个null.方法里面其实已经判断了.如果没有获取对象.就会new一个Message给你.

3.MessageQueue(消息队列)
MessageQueue是消息队列的意思.它的主要用于存放所有通过Handler发送的Message对象.这一部分的消息会在消息队列里面等待Looper(轮询器)来处理.
原理:其以队列的形式对外提供插入和删除的工作,虽然叫做消息队列,但是它的内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表
MessageQueue中只存在一个Message对象.但是这个对象里面又有下一个对象的引用.这就是单链表

#每个线程中只会有一个Looper和MessageQueue.

4.Looper
1.与当前线程绑定,保证一个线程只会有一个Looper实例,同时一个Looper实例也只有一个MessageQueue
2.Looper的作用就是不断循环从MessageQueue获得消息.并且调用Handler的handleMessage(Msg) 方法.把获取的消息作为参数传递过去.所以为什么Looper能这么轻松的操作MessageQueue.

另外有几个小细节的地方.'
1.为什么Looper存在于主线程又可以不断循环地从查询MessageQuery
其实有一个休眠的机制.当MessageQuery里面为空的时候.Looper就会进入休眠.但是当有新的信息进入队列中的时候.队列就会通知Looper来获取Message.

2.创建Handler对象之前.要先需要创建Looper对象.否则会报错
1.为什么主线程中创建Handler对象不报错.看源码知道.那是因为在程序开始的时候.系统已经自动帮我们调用了Looper.prepareMainLooper(). 
  而prepareMainLooper().这个方法.就会调用Looper.prepare(). 


5.HandlerThread
通过源码发现.HandlerThread是继承Thread类的.那么里面做了什么呢.它重写了一个run()方法并且暴露了一个onLooperPre其实就是准备了一个轮询器并且开启这个轮询器的loop()方法
简单来说就是帮一个新线程封装了一个轮询器.不用自己新建线程的时候手动在准备轮询器了.
应用场景:
我们以前新建子线程的时候.都会自己自己去准备轮询器.当我们使用这个HandlerThread的时候.就不用去准备轮询器了.\\


重点:
思考一个问题.每次创建looper对象都会调用Looper.prepare()方法.而这个Looper.prepare().
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));
}
我们发现每一次为这个线程准备轮询器.的时候都会调用静态sThreadLocal.set一个对象进去.
那么我们不同线程调用这个方法的时候.是怎么考虑到线程间的存储.其实是用了一个特殊类.
这个特殊类(ThreadLocal)保存的数据就是区分线程的.(这个类下面会说)


额外:
最后将一下Handler的post()方法.他内部是怎么做到把线程任务放到主线程来做的.
简单说一下.其实当我们调用post()的时候.在内部会创建一个Message的对象.通过
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
在Message对象里面一个callback(Runnble)对象.赋值成我们的Runnable对象,

public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
转换成Message对象之后发送出去-放到消息列表中 
public void dispatchMessage(Message msg) {
if (msg.callback != null) {// 如果当前msg对象有callback(线程任务)的话.我们就调用handleCallback()方法
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
如果当前msg对象含有线程任务的话.我们就调用handlerCallback()方法

private static void handleCallback(Message message) {
message.callback.run();
}
最后发现.到主线程之后.就直接.run()去执行了.这就是post的原理.



ThreadLocal类

     Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的MessageQueue和Looper的支撑。MessageQueue的中文翻译是消息队列,顾名思义它的内部存储了一组消息,其以队列的形式对外提供插入和删除的工作,虽然叫做消息队列,但是它的内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表。Looper的中文翻译为循环,在这里可以理解为消息循环,由于MessageQueue只是一个消息的存储单元,它不能去处理消息,而Looper就填补了这个功能,Looper会以无限循环的形式去查找是否有新消息,如果有的话就处理消息,否则就一直等待着。Looper中还有一个特殊的概念,那就是ThreadLocal,ThreadLocal并不是线程,它的作用是可以在每个线程中存储数据。大家知道,Handler创建的时候会采用当前线程的Looper来构造消息循环系统,那么Handler内部如何获取到当前线程的Looper呢?这就要使用ThreadLocal了,ThreadLocal可以在不同的线程之中互不干扰地存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper。当然需要注意的是,线程是默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。大家经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。


    ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其它线程来说无法获取到数据。在日常开发中用到ThreadLocal的地方较少,但是在某些特殊的场景下,通过ThreadLocal可以轻松地实现一些看起来很复杂的功能,这一点在Android的源码中也有所体现,比如Looper、ActivityThread以及AMS中都用到了ThreadLocal。具体到ThreadLocal的使用场景,这个不好统一地来描述,一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。比如对于Handler来说,它需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取,如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定线程的Looper,这样一来就必须提供一个类似于LooperManager的类了,但是系统并没有这么做而是选择了ThreadLocal,这就是ThreadLocal的好处。


   介绍了那么多ThreadLocal的知识,可能还是有点抽象,下面通过实际的例子为大家演示ThreadLocal的真正含义。首先定义一个ThreadLocal对象,这里选择Boolean类型的,如下所示:

private ThreadLocal<Boolean>mBooleanThreadLocal = new ThreadLocal<Boolean>();

然后分别在主线程、子线程1和子线程2中设置和访问它的值,代码如下所示:

mBooleanThreadLocal.set(true);
Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());

new Thread("Thread#1") {
	@Override
	public void run() {
		mBooleanThreadLocal.set(false);
		Log.d(TAG, "[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
	};
}.start();

new Thread("Thread#2") {
	@Override
	public void run() {
		Log.d(TAG, "[Thread#2]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
	};
}.start();

       在上面的代码中,在主线程中设置mBooleanThreadLocal的值为true,在子线程1中设置mBooleanThreadLocal的值为false,在子线程2中不设置mBooleanThreadLocal的值,然后分别在3个线程中通过get方法去mBooleanThreadLocal的值,根据前面对ThreadLocal的描述,这个时候,主线程中应该是true,子线程1中应该是false,而子线程2中由于没有设置值,所以应该是null,安装并运行程序,日志如下所示:

D/TestActivity(8676):[Thread#main]mBooleanThreadLocal=true

D/TestActivity(8676):[Thread#1]mBooleanThreadLocal=false

D/TestActivity(8676):[Thread#2]mBooleanThreadLocal=null

      从上面日志可以看出,虽然在不同线程中访问的是同一个ThreadLocal对象,但是它们通过ThreadLocal来获取到的值却是不一样的,这就是ThreadLocal的奇妙之处。结合这这个例子然后再看一遍前面对ThreadLocal的两个使用场景的理论分析,大家应该就能比较好地理解ThreadLocal的使用方法了。ThreadLocal之所以有这么奇妙的效果,是因为不同线程访问同一个ThreadLocal的get方法,ThreadLocal内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找出对应的value值,很显然,不同线程中的数组是不同的,这就是为什么通过ThreadLocal可以在不同的线程中维护一套数据的副本并且彼此互不干扰。





  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值