Android Handler机制的工作原理详析
写在前面
上一次写完Binder学习笔记之后,再去看一遍Activity的启动流程,因为了解了Binder的基本原理,这次看印象会更深一点,学习效果也比以前好很多。本来打算直接来写Activity的启动流程的,但总觉得Handler也需要写一下,知道Handler和Binder的原理后,再去看Activity的启动流程,应该也没什么问题了。虽然网上已经有很多Handler相关的文章了,而且Handler机制的上层原理也并不难,还是决定写一下,因为我想构建自己的知识体系。也希望给看我博客的朋友们一个无缝衔接的阅读体验。
Handler机制涉及到的类主要有Handler、Message、Looper、MessageQueue、ThreadLocal等。虽然我们最熟悉的是Handler和Message这两个类,但是在我们开始可以使用Handler之前,Looper是为我们做了一些事情的。
本文的源码是基于android-28的
Looper
在使用Handler之前,我们必须得初始化Looper,并让Looper跑起来。
Looper.prepare(); ... Looper.loop();
执行上面两条语句之后,Looper就可以跑起来了。先来看看对应的源码:
publicstaticvoidprepare(){
prepare(true);
}
privatestaticvoidprepare(booleanquitAllowed){
if(sThreadLocal.get()!=null){
thrownewRuntimeException("OnlyoneLoopermaybecreatedperthread");
}
sThreadLocal.set(newLooper(quitAllowed));
}
privateLooper(booleanquitAllowed){
mQueue=newMessageQueue(quitAllowed);
mThread=Thread.currentThread();
}
必须保证一个线程中有且只有一个Looper对象,所以在初始化Looper的时候,会检查当前线程有没有Looper对象。Looper的初始化会创建一个MessageQueue。创建完Looper后会放到ThreadLocal中去,关于ThreadLocal,后面会说到。
publicstaticvoidloop(){
//判断当前线程有没有初始化Looper
finalLooperme=myLooper();
if(me==null){
thrownewRuntimeException("NoLooper;Looper.prepare()wasn'tcalledonthisthread.");
}
finalMessageQueuequeue=me.mQueue;
...
for(;;){
Messagemsg=queue.next();//mightblock
if(msg==null){
//Nomessageindicatesthatthemessagequeueisquitting.
return;
}
...
finallongtraceTag=me.mTraceTag;
if(traceTag!=0&&Trace.isTagEnabled(traceTag)){
Trace.traceBegin(traceTag,msg.target.getTraceName(msg));
}
try{
//target指的是Handler
msg.target.dispatchMessage(msg);
}finally{
if(traceTag!=0){
Trace.traceEnd(traceTag);
}
}
...
msg.recycleUnchecked();
}
}
方法比较长,所以只把最核心的代码放了出来。省略掉的代码中有一个比较有意思的:我们可以指定一个阈值比如说200,当Message的处理超过200ms时,就会输出Log。这可以在开发中帮助我们发现一些潜在的性能问题。可惜的是,设置阈值的方法是隐藏的,无法直接调用,所以这里就不放出代码了,感兴趣的朋友自己翻一下源码吧。
简化后的代码可以看出逻辑十分简单,可以说Looper在当中扮演着搬砖工的角色,从MessageQueue中取出Message,然后交给Handler去分发,再去MessageQueue中取出Message...无穷无尽,就像愚公移山一样。
看到这里,应该多多少少会觉得有点不对劲,因为这里是一个死循环,按道理来说会一直占着CPU资源的,并且消息也总有处理完的时候,难道处理完就从消息队列返回Null,然后Looper结束吗?显然不是,注意看注释mightblock。
MessageQueue
答案就在MessageQueue里面,直接来看一下next():
Messagenext(){
...
intpendingIdleHandlerCount=-1;//-1onlyduringfirstiteration
intnextPollTimeoutMillis=0;
for(;;){
if(nextPollTimeoutMillis!=0){
Binder.flushPendingCommands();
}
nativePollOnce(ptr,nextPollTimeoutMillis);
synchronized(this){
//Trytoretrievethenextmessage.Returniffound.
finallongnow=SystemClock.uptimeMillis();
MessageprevMsg=null;
Messagemsg=mMessages;
if(msg!=null&&msg.target==null){
//Stalledbyabarrier.Findthenextasynchronousmessageinthequeue.
do{
prevMsg=msg;
msg=msg.next;
}while(msg!=null&&!msg.isAsynchronous());
}
if(msg!=null){
if(now
代码有点长,这次不打算省略掉一些了,因为这里面还有一个小彩蛋。
方法中最重要的应该就是这一行了
nativePollOnce(ptr,nextPollTimeoutMillis);
简单来说,当nextPollTimeoutMillis==-1时,挂起当前线程,释放CPU资源,当nextPollTimeoutMillis>=0时会延时指定的时间激活一次线程,让代码继续执行下去。这里涉及到了底层的pipe管道和epoll机制,就不再讲下去了(其实是因为讲不下去了)。这也就可以回答上面的问题了,当没有消息的时候只需要让线程挂起就行了,这样可以保证不占用CPU资源的同时保住Looper的死循环。
然后我们来看消息是如何取出来的。MessageQueue中有一个Message,Message类中又有一个Message成员next,可以看出Message是一个单链表结构。消息的顺序是根据时间先后顺序排列的。一般来说,我们要取的Message就是第一个(这里先不考虑异步消息,关于异步消息以后会讲到的,又成功给自己挖了一个坑哈哈),如果当前时间大于等于Message中指定的时间,那么将消息取出来,返回给Looper。由于此时nextPollTimeoutMillis的值为0,所以当前面的消息处理完之后,Looper就又来取消息了。
如果当前的时间小于Message中指定的时间,那么设置nextPollTimeoutMillis值以便下次唤醒。还有另外一种当前已经没有消息了,nextPollTimeoutMillis会被设置为-1,也就是挂起线程。别急,还没那么快呢,接着往下看。
紧接着的逻辑是判断当前有没有IdleHandler,没有的话就continue,该挂起就挂起,该延时就延时,有IdleHandler的话会执行它的queueIdle()方法。这个IdleHandler是干什么的呢?从名字应该也能猜出个一二来,这里就不再展开讲了。关于它的一些妙用可以看我之前写的Android启动优化之延时加载。执行完queueIdle()方法后,会将nextPollTimeoutMillis置为0,重新看一下消息队列中有没有新的消息。
Handler
上面将取消息的流程都讲清楚了,万事俱备,就差往消息队列中添加消息了,该我们最熟悉的Handler出场了。Handler往队列中添加消息,主要有两种方式:
Handler.sendXXX();
Handler.postXXX();
第一种主要是发送Message,第二种是Runnable。无论是哪种方式,最终都会进入到MessageQueue的enqueueMessage()方法。
booleanenqueueMessage(Messagemsg,longwhen){
...
synchronized(this){
...
msg.markInUse();
msg.when=when;
Messagep=mMessages;
booleanneedWake;
if(p==null||when==0||when
一般情况下,我们通过Handler发送消息的时候,会通过SystemClock.uptimeMillis()获取一个开机时间,然后MessageQueue就会根据这个时间来对Message进行排序。所以enqueueMessage()方法中就分了两种情况,一种是直接可以在队头插入的。一种是排在中间,需要遍历一下,然后寻一个合适的坑插入。when==0对应的是Handler的sendMessageAtFrontOfQueue()和postAtFrontOfQueue()方法。needWake的作用是根据情况唤醒Looper线程。
上面有一点还没有讲,就是Looper从MessageQueue中取出Message后,会交由Handler进行消息的分发。
publicvoiddispatchMessage(Messagemsg){
if(msg.callback!=null){
handleCallback(msg);
}else{
if(mCallback!=null){
if(mCallback.handleMessage(msg)){
return;
}
}
handleMessage(msg);
}
}
优先级顺序是Message自带的callback,接着是Handler自带的callback,最后才是handleMessage()这个回调。
ThreadLocal
还记得Looper中有一个ThreadLocal吧,把它放到最后来讲是因为它可以单独拿出来讲,不想在上面干扰到整个流程。
ThreadLocal是一个数据存储类,它最神奇的地方就是明明是同一个ThreadLocal对象,但是在不同线程中可以存储不同的对象,比如说在线程A中存储了"Hello",而在线程B中存储了"World"。它们之间互相不干扰。
在Handler机制中,由于一个Looper对应着一个线程,所以将Looper存进ThreadLocal最合适不过了。
ThreadLocal比价常用的就set()和get()方法。分别来看看怎么实现的吧。
publicvoidset(Tvalue){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
if(map!=null)
map.set(this,value);
else
createMap(t,value);
}
首先是去获取ThreadLocalMap,找得到的话直接设置值,找不到就创建一个。
ThreadLocalMapgetMap(Threadt){
returnt.threadLocals;
}
看到这里,大概也能明白了。每个线程Thread中有一个ThreadLocalMap对象。通过ThreadLocal.set()方法,实际上是去获取当前线程中的ThreadLocalMap,线程不同,获取到的ThreadLocalMap自然也不同。
再来看看这个ThreadLocalMap是什么来头。看类的注释中有这么一句话:
ThreadLocalMapisacustomizedhashmapsuitableonlyformaintainingthreadlocalvalues.
从注释中可以知道这就是一个定制的HashMap,并且它的Entry类指定了Key只能为ThreadLocal类型的。所以直接将它看成是一个HashMap就好了。
get()方法也好理解,就是从Map中取出值而已。大概看一下就好了。
publicTget(){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
if(map!=null){
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
@SuppressWarnings("unchecked")
Tresult=(T)e.value;
returnresult;
}
}
returnsetInitialValue();
}
写在最后
虽然在开始写之前,觉得Handler机制比较简单,好像没啥必要写,但真正要写起来的时候还是得去深入了解代码的细节,然后才发现有些地方以前理解得也不够好。能理解和能写出来让别人理解,其实是不同的层次了。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。