消息机制Handler
<h3><b>基本概述</b></h3>
在Android开发中提到消息机制应该所有人都不陌生,但是估计也很少有人能把消息机制详细地说出个所以然来,我们在开发过程中,有很多技术痛点,就是明明很多时候我们在用的东西,我们却不知道是为什么,它具体是个什么东西,很多时候我们只在乎它如何被拿来用,而不去关心更深层次的东西,也许这就是初中级向上进阶的阻碍吧。还好,我意识到了这一点,尽力去了解更多事实的真相...一不小心说了这么多废话,言归正传吧。
在Android中,我们提到的消息机制常常指的就是Handler机制,Handler是Android中的上层接口,在开发的过程中,我们可以利用Handler将一件事情切换到Handler所在的线程中去执行(好好理解这句话)。为什么会有这样的机制出现?我们来考虑一下,在实际的操作中,我们发现,Handler机制的运用主要是在UI更新上,当我们需要做一个耗时操作时,我们不可能在mainThread中做这个事情,因为ANR的存在,这时我们需要开启一个新的线程去做这个事情,当事件完成后再通过Handler去告诉mainThread现在可以更新UI了。这就是对消息机制的一个很宽泛的描述。这时候,我们应该考虑一个问题?为什么我们不干脆在开启的新线程中更新UI呢,应该很多人会说是因为Android不允许这样做?那么, 为什么不允许呢?
如果不限制UI的更新规则(即可在任何线程中操作UI)会带来什么样的坏处?如果所有线程都可以改变UI,那我们如何保证UI朝着我们预期的方向发展?这就像是共享内存一样,同时的操作会让UI的状态变得不可预期。那么,如果加上锁机制呢?这样必定会大大降低UI的访问效率,因为锁机制会阻塞线程,就会让我们的应用在很多情况下看起来像失了智一样,卡顿严重,所以Handler机制油然而生了。
<h3><b>工作原理</b></h3>
Android的Handler机制它的工作原理是怎样的呢?我们在线程中创建一个Handler对象,当使用它的时候会发现系统会报这样的错:
can not create handler inside thread that has not called Looper.prepare();
它描述了在一个线程中我们不能在没有Looper的前提下去使用Handler,也就是说Handler机制的实现需要Looper这个东西,那么Looper是啥呢?我们可以通过Looper.prepare()在当前线程中创建一个Looper对象,当我们查阅源码的时候可以发现,Looper.prepare()做了什么,实际上它创建了一个MessageQueue(消息队列)。Looper相关源码如下:
public static void prepare(){
prepare(true);
}
public static void prepare(boolean quitAllowed){
if( sThreadLocal.get()!=null ){
...
}
sThreadLocal.set(new Looper(quitAllowed));
}
public Looper(){
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
在创建了Looper之后,我们不仅仅得到了looper对象,还得到了一个消息队列messageQueue,这时候才构建了一个完整的Handler机制。它们是如何分工的。
线程内部负责处理业务逻辑,looper的作用负责消息的存取查阅,其中messageQueue是消息的存储结构,它是单链表形式,looper负责消息的轮询,handler负责发送和处理消息。looper、线程、messageQueue是一一对应的,在不同的线程中它们各自有着对应的模块。我们还能看到创建的looper对象被放入了ThreadLocal中去,这样的作用是,各个线程中的Looper对象是相对独立,谁也影响不到谁,如何达到这个效果的呢?我们就来看一下ThreadLocal这个类。
在实际使用的过程中我们发现,运行在主线程的Handler我们不需要事先为它创建一个Looper来进行消息管理,这是为什么?这是因为系统早已经默认给主线程创建了Looper对象。
<h3><b>ThreadLocal类</b></h3>
从它的名字来看,它似乎是个什么线程,其实不然ThreadLocal不是线程但和线程有关,具体的来讲,它是一个和线程相关的存储方式。它是线程内部的存储方式,只有在当前线程内可以获得这个值。它的内部是如何实现的?我们通过查看源码不难发现,它是一个类似于数组且以key-value方式存储的一种数据格式。
ThreadLocal内部存储是数组结构,它以当前线程作为Key来区别各个线程之间的数据,查看源码可以发现,在偶数位它用来存放当前线程,在此的后一位来存放具体的数据。所以通过不同的线程可以定位到不同位置的key,然后向后延一位就能得到存储的数据,这就是ThreadLocal能做到各个线程间互不干涉的所在。它最典型的应用就是Handler机制中Looper的构建。
我们下面来举个很简单的例子,我们来看伪代码:
private ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>;
// 主线程
mThreadLocal.set(true);
Log.i("threadLocal", mThreadLocal.get());
// 线程1
new Thread("Thread1"){
@ovrride
public void run(){
mThreadLocal.set(false);
Log.i("threadLocal", mThreadLocal.get());
}
}
// 线程2
new Thread("Thread2"){
@ovrride
public void run(){
Log("threadLocal", mThreadLocal.get());
}
}
上面的例子中,我们分别在主线程,线程1,线程2中对mThreadLocal变量进行了操作,并且在这三个地方分别打印该变量的值,如果是普通的变量,那么我想这些打印的值恐怕只能靠猜了...,而现在,我们了解了ThreadLocal的存储原理,不难分析出各个位置打印的值。
因为它们不会相互影响, 所以在主线程中设置了true,那么我以主线程去保存数据,我在打印的时候还以主线程为key去获取对应的值,这个值一定是当时存储的值true。同样的判断,在线程1中打印的值是false。而在线程2中,我们从来没有在对应的存储空间上为线程2来开辟存储空间,所以它的值应该是null。
<h3><b>消息队列</b></h3>
我们在上面的介绍中可以知道,Looper对象是在其内部创建了一个消息队列用来存储消息的,所以在了解Looper之前,我们有必要知道消息队列的一些事情。
消息队列在Android中指的就是MessageQueue,从它的名字上来看是个队列,实际上它的内部实现不是队列,而是单链表结构的,它提供了消息机制中消息的插入和读取。
说点其他的,我们应该知道链表的概念,链表的概念是从C语言中引用而来的,它表示每个消息是相连的,每一个消息在链表中称作一个节点,节点不仅仅保存数据,它还保存了一个指针用来指向下一个节点的地址,所以它的优点在可以高效率地插入、删除。因为消息队列常用的操作就是插入消息和取出消息,而取出消息实质上就是将该节点从数据结构中剔除,所以消息队列会选取链表结构来存储消息。
<h3><b>Looper</b></h3>
我们从上面的分析中可以知道,Handler的工作需要Looper,也知道可以通过Looper.prepare()为Handler创建一个Looper。我们通过Looper.loop()开启Looper轮询,此时Looper会不停的轮询消息队列中的数据,一有消息通过Handler发出时,Looper就能捕获这个消息,并通过这个函数来处理:
msg.target.dispatchMessage(msg)
其实,msg.target实际上就是发出消息的那个Handler,调用Handler的dispatchMessage(msg)函数来最终处理这个消息。
每个线程都有自己的Looper,非主线程我们是不可预期的,但是主线程永远只有一个,所以我们可以通过静态方法Looper.getMainLooper()随时获取主线程的Looper。
我们说过,如果消息队列中不存在消息时,Looper将会处于阻塞状态,这无疑增加了系统的开销,所以当我们确定不再有消息传递进来时,可以通过Looper.quit()或Looper.quitSately()方法来退出Looper。前者是一调用立马退出Looper,后者是需要将现有的消息处理完之后再退出Looper,相对来讲后者更有责任心一些,但这也不一定就是必要的。
<h3><b>Handler</b></h3>
说了这么多是时候来认识最后的东西了。从以上种种我们的介绍,我们可以很清晰地知道,在Handler消息机制中,Handler做扮演的角色的作用就是发送和接收消息。
我们经常性的操作来用语言描述一次。我们在主线程中定了一个Handler,由于系统已经默认帮我们把Looper创建好了,所以我们不需要再去创建Looper。这时候我们需要向服务器进行一个耗时的请求,那么我们开启了线程a去请求数据,当数据请求完成后,我们希望数据可以在UI上(即主线程)上展示,所以我们利用Handler将线程a取到的数据发送出去。Looper循环机制接收并处理了该消息,最终该消息交由Handler所在的线程(主线程)处理。我们通过Handler机制将UI操作从线程a中搬到了主线程中去。
那么Handler是如何处理消息的?我们看到 msg.target.dispatchMessage(msg)
这个函数,略过长篇大论的判断和callback,在日常开发中,创建Handler最常用的就是派生一个Handler的子类,并重写handlerMessage()方法来处理具体的消息。
<h3><b>进一步认清Handler</b></h3>
当我们了解以上Android的消息机制后,我们不禁沾沾自喜,偶尔看到了这样的代码:
new Handler().postDelayed(new Runnable(){
@Override
public void run(){
Toast.makeText(mContext, "hellow", Toast.LENGHT_LONG).show();
}
}, 1000);
乍一看,咦?在非主线程中更新UI?竟然不报错...,好吧我们来分析一下吧。首先我们来看Handler的构造函数。
1. Handler的构造函数
public Handler(){
this(null, false);
}
public Handler(Callback callback, boolean async) {
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
此时Handler做了一些准备工作,比如准备Looper等,此时一个变量mCallback==null。此时我们将Looper中的队列和Handler中的队列关联起来。
2. 我们再来看postDelayed()函数
public final boolean postDelayed(Runnable r, long delayMillis){
return sendMessageDelayed(getPostMessage(r), delayMillis);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
很显然我们将Runnable这个线程设置为了Message的callback属性。我们经过跟踪,依次调用了这些方法。
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
从 msg.target = this;
这句话看出msg.target就是Handler,也就是说此时MessageQueue和Handler发生了联系,这个enqueueMessage方法很显然就是我们在MessageQueue中讲的入列方法,即将消息加入到了对应线程的Looper消息队列中去了,此时Looper获取到了这个消息,并且我们上面讲Looper时提到了Looper调用这个函数来处理消息msg.target.dispatchMessage(msg)
,msg.target是什么?就是Handler啊,所以这个消息由Handler的dispatchMessage(msg)函数来处理消息。
3. dispatchMessage()方法
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
通过步骤2可以知道,msg.callback!=null,而是runnable。所以执行handlerCallback(msg);
4. handlerCallback(msg)方法
private static void handleCallback(Message message) {
message.callback.run();
}
这样,就会执行我们在Activity 的Runnable 中的run 方法了,也就是显示Toast。所以为什么这个Toast是没问题的呢?
这是因为,我们将Runnable设置给了Message的属性callback,这个callback连同Message被传递到Handler所在的线程(即UI线程)中去了,所以它实际上试运行在UI线程上的。
至此,我们应该对Handler机制有了比较深刻的理解了。
如果有你认为不恰当的描述,不妨留言告诉我...