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机制比较简单,好像没啥必要写,但真正要写起来的时候还是得去深入了解代码的细节,然后才发现有些地方以前理解得也不够好。能理解和能写出来让别人理解,其实是不同的层次了。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。