不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!
handler
机制几乎是Android
面试时必问的问题,虽然看过很多次handler
源码,但是有些面试官问的问题却不一定能够回答出来,趁着机会总结一下面试中所覆盖的handler
知识点。
下面的这幅图很完整的表现了整个handler
机制。
要理解handler的实现原理,其实最重要的是理解Looper
的实现原理,Looper
才是实现handler
机制的核心。任何一个handler
在使用sendMessage
或者post
时候,都是先构造一个Message
,并把自己放到message中
,然后把Message
放到对应的Looper
的MessageQueue
,Looper
通过控制MessageQueue
来获取message
执行其中的handler
或者runnable
。 要在当前线程中执行handler
指定操作,必须要先看当前线程中有没有looper
,如果有looper
,handler
就会通过sendMessage
,或者post
先构造一个message
,然后把message
放到当前线程的looper
中,looper
会在当前线程中循环取出message
执行,如果没有looper
,就要通过looper.prepare()
方法在当前线程中构建一个looper
,然后主动执行looper.loop()
来实现循环。
梳理一下其实最简单的就下面四条:
1、每一个线程中最多只有一个Looper
,通过ThreadLocal
来保存,Looper
中有Message
队列,保存handler
并且执行handler
发送的message
。
2、在线程中通过Looper.prepare()
来创建Looper
,并且通过ThreadLocal
来保存Looper
,每一个线程中只能调用一次Looper.prepare()
,也就是说一个线程中最多只有一个Looper
,这样可以保证线程中Looper
的唯一性。
3、handler
中执行sendMessage
或者post
操作,这些操作执行的线程是handler
中Looper
所在的线程,和handler
在哪里创建没关系,和Handler
中的Looper
在那创建有关系。
4、一个线程中只能有一个Looper
,但是一个Looper
可以对应多个handler
,在同一个Looper
中的消息都在同一条线程中执行。
要看sendMessage
和post
区别,需要从源码来看,下面是几种使用handler
的方式,先看下这些方式,然后再从源码分析有什么区别。例1、 主线程中使用handler
//主线程HandlermHandler=newHandler(newHandler.Callback(){@OverridepublicbooleanhandleMessage(@NonNullMessagemsg){if(msg.what==1){//doingsomething}returnfalse;}});Messagemsg=Message.obtain();msg.what=1;mHandler.sendMessage(msg);
上面是在主线程中使用handler
,因为在Android
中系统已经在主线程中生成了Looper
,所以不需要自己来进行looper
的生成。如果上面的代码在子线程中执行,就会报
Can'tcreatehandlerinsidethread"+Thread.currentThread()+"thathasnotcalledLooper.prepare()
如果想着子线程中处理handler
的操作,就要必须要自己生成Looper
了。
例2 、子线程中使用handler
Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){Looper.prepare();Handlerhandler=newHandler();handler.post(newRunnable(){@Overridepublicvoidrun(){}});Looper.loop();}});
上面在Thread
中使用handler
,先执行Looper.prepare
方法,来在当前线程中生成一个Looper
对象并保存在当前线程的ThreadLocal
中。 看下Looper.prepare()
中的源码:
//prepareprivatestaticvoidprepare(booleanquitAllowed){if(sThreadLocal.get()!=null){thrownewRuntimeException("OnlyoneLoopermaybecreatedperthread");}sThreadLocal.set(newLooper(quitAllowed));}//LooperprivateLooper(booleanquitAllowed){mQueue=newMessageQueue(quitAllowed);mThread=Thread.currentThread();}
可以看到prepare
方法中会先从sThreadLocal
中取如果之前已经生成过Looper
就会报错,否则就会生成一个新的Looper
并且保存在线程的ThreadLocal
中,这样可以确保每一个线程中只能有一个唯一的Looper
。
另外:由于Looper
中拥有当前线程的引用,所以有时候可以用Looper
的这种特点来判断当前线程是不是主线程。
@RequiresApi(api=Build.VERSION_CODES.KITKAT)booleanisMainThread(){returnObjects.requireNonNull(Looper.myLooper()).getThread()==Looper.getMainLooper().getThread();}
sendMessage vs post
先来看看sendMessage
的代码调用链:
enqueueMessage
源码如下:
privatebooleanenqueueMessage(@NonNullMessageQueuequeue,@NonNullMessagemsg,longuptimeMillis){msg.target=this;msg.workSourceUid=ThreadLocalWorkSource.getUid();returnqueue.enqueueMessage(msg,uptimeMillis);}
enqueueMessage
的代码处理很简单,msg.target = this;
就是把当前的handler
对象给message.target
。然后再讲message
进入到队列中。
post代码调用链:
调用post
时候会先调用getPostMessage
生成一个Message
,后面和sendMessage
的流程一样。下面看下getPostMessage
方法的源码:
privatestaticMessagegetPostMessage(Runnabler){Messagem=Message.obtain();m.callback=r;returnm;}
可以看到getPostMessage
中会先生成一个Messgae
,并且把runnable
赋值给message
的callback.
消息都放到MessageQueue
中后,看下Looper
是如何处理的。
for(;;){Messagemsg=queue.next();//mightblockif(msg==null){return;}msg.target.dispatchMessage(msg);}
Looper
中会遍历message
列表,当message
不为null
时调用msg.target.dispatchMessage(msg)
方法。看下message
结构:
也就是说msg.target.dispatchMessage
方法其实就是调用的Handler中的dispatchMessage
方法,下面看下dispatchMessage
方法的源码:
publicvoiddispatchMessage(@NonNullMessagemsg){if(msg.callback!=null){handleCallback(msg);}else{if(mCallback!=null){if(mCallback.handleMessage(msg)){return;}}handleMessage(msg);}}//privatestaticvoidhandleCallback(Messagemessage){message.callback.run();}
因为调用post
方法时生成的message.callback=runnable
,所以dispatchMessage
方法中会直接调用message.callback.run();
也就是说直接执行post
中的runnable
方法。 而sendMessage
中如果mCallback
不为null
就会调用mCallback.handleMessage(msg)
方法,否则会直接调用handleMessage
方法。
总结post
方法和handleMessage
方法的不同在于,post
的runnable
会直接在callback
中调用run
方法执行,而sendMessage
方法要用户主动重写mCallback
或者handleMessage
方法来处理。
首先给出结论,Looper
不会一直消耗系统资源,当Looper
的MessageQueue
中没有消息时,或者定时消息没到执行时间时,当前持有Looper
的线程就会进入阻塞状态。
下面看下looper
所在的线程是如何进入阻塞状态的。looper
阻塞肯定跟消息出队有关,因此看下消息出队的代码。
消息出队
Messagenext(){//Returnhereifthemessageloophasalreadyquitandbeendisposed.//Thiscanhappeniftheapplicationtriestorestartalooperafterquit//whichisnotsupported.finallongptr=mPtr;if(ptr==0){returnnull;}intnextPollTimeoutMillis=0;for(;;){if(nextPollTimeoutMillis!=0){Binder.flushPendingCommands();}nativePollOnce(ptr,nextPollTimeoutMillis);//Whilecallinganidlehandler,anewmessagecouldhavebeendelivered//sogobackandlookagainforapendingmessagewithoutwaiting.if(hasNoMessage){nextPollTimeoutMillis=-1;}}}
上面的消息出队方法被简写了,主要看下面这段,没有消息的时候nextPollTimeoutMillis=-1
;
if(hasNoMessage){nextPollTimeoutMillis=-1;}
看for循环里面这个字段所其的作用:
if(nextPollTimeoutMillis!=0){Binder.flushPendingCommands();}nativePollOnce(ptr,nextPollTimeoutMillis);
Binder.flushPendingCommands();
这个方法的作用可以看源码里面给出的解释:
/***FlushanyBindercommandspendinginthecurrentthreadtothekernel*driver.Thiscanbe*usefultocallbeforeperforminganoperationthatmayblockforalong*time,toensurethatanypendingobjectreferenceshavebeenreleased*inordertopreventtheprocessfromholdingontoobjectslongerthan*itneedsto.*/
也就是说在用户线程要进入阻塞之前跟内核线程发送消息,防止用户线程长时间的持有某个对象。再看看下面这个方法:nativePollOnce(ptr, nextPollTimeoutMillis);
当nextPollingTimeOutMillis=-1
时,这个native
方法会阻塞当前线程,线程阻塞后,等下次有消息入队才会重新进入可运行状态,所以Looper
并不会一直死循环消耗运行内存,对队列中的颜色消息还没到时间时也会阻塞当前线程,但是会有一个阻塞时间也就是nextPollingTimeOutMillis>0
的时间。
当消息队列中没有消息的时候looper肯定是被消息入队唤醒的。
消息入队
booleanenqueueMessage(Messagemsg,longwhen){if(msg.target==null){thrownewIllegalArgumentException("Messagemusthaveatarget.");}if(msg.isInUse()){thrownewIllegalStateException(msg+"Thismessageisalreadyinuse.");}synchronized(this){if(mQuitting){IllegalStateExceptione=newIllegalStateException(msg.target+"sendingmessagetoaHandleronadeadthread");Log.w(TAG,e.getMessage(),e);msg.recycle();returnfalse;}msg.markInUse();msg.when=when;Messagep=mMessages;booleanneedWake;if(p==null||when==0||when<p.when){//Newhead,wakeuptheeventqueueifblocked.msg.next=p;mMessages=msg;needWake=mBlocked;}else{//Insertedwithinthemiddleofthequeue.Usuallywedon'thavetowake//uptheeventqueueunlessthereisabarrierattheheadofthequeue//andthemessageistheearliestasynchronousmessageinthequeue.needWake=mBlocked&&p.target==null&&msg.isAsynchronous();Messageprev;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.nextprev.next=msg;}//WecanassumemPtr!=0becausemQuittingisfalse.if(needWake){nativeWake(mPtr);}}returntrue;}
上面可以看到消息入队之后会有一个
if(needWake){nativeWake(mPtr);}
方法,调用这个方法就可以唤醒线程了。另外消息入队的时候是根据消息的delay
时间来在链表中排序的,delay
时间长的排在后面,时间短的排在前面。如果时间相同那么按插入时间先后来排,插入时间早的在前面,插入时间晚的在后面。
Looper
是如何判断Message
是从哪个handler
传来的呢?其实很简单,在1
中分析过,handler
在sendMessage
的时候会构建一个Message
对象,并且把自己放在Message
的target
里面,这样的话Looper
就可以根据Message
中的target
来判断当前的消息是哪个handler
传来的。
从3中知道在消息出队的for
循环队列中会调用到下面的方法。
nativePollOnce(ptr,nextPollTimeoutMillis);
如果是延时消息,会在被阻塞nextPollTimeoutMillis
时间后被叫醒,nextPollTimeoutMillis
就是消息要执行的时间和当前的时间差。
在子线程中,如果手动为其创建Looper
,那么在所有的事情完成以后应该调用quit
方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper
以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper
。
Looper.myLooper().quit()
那么,如果在Handler
的handleMessage
方法中(或者是run方法)处理消息,如果这个是一个延时消息,会一直保存在主线程的消息队列里,并且会影响系统对Activity
的回收,造成内存泄露。
具体可以参考Handler
内存泄漏分析及解决
总结一下,解决Handler
内存泄露主要2点
1 、有延时消息,要在Activity
销毁的时候移除Messages
2、 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity
使用弱引用。
Looper
是保存在线程的ThreadLocal
里面的,使用Handler
的时候要调用Looper.prepare()
来创建一个Looper
并放在当前的线程的ThreadLocal
里面。
privatestaticvoidprepare(booleanquitAllowed){if(sThreadLocal.get()!=null){thrownewRuntimeException("OnlyoneLoopermaybecreatedperthread");}sThreadLocal.set(newLooper(quitAllowed));}
可以看到,如果多次调用prepare
的时候就会报Only one Looper may be created per thread
,所以这样就可以保证一个线程中只有唯一的一个Looper
。
handler
的执行跟创建handler
的线程无关,跟创建looper
的线程相关,加入在子线程中创建一个Handler
,但是Handler
相关的Looper
是主线程的,这样,如果handler
执行post
一个runnable
,或者sendMessage
,最终的handle Message
都是在主线程中执行的。
Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){Looper.prepare();Handlerhandler=newHandler(getMainLooper());handler.post(newRunnable(){@Overridepublicvoidrun(){Toast.makeText(MainActivity.this,"hello,world",Toast.LENGTH_LONG).show();}});Looper.loop();}});thread.start();心里话
不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊~
如果文字版的handle
汇总还有些不懂得话,我给大家准备了三星架构师讲解的2
小时视频,Handler
面试需要的所有知识都在这,可以好好学一学!
当然,面试的时候肯定不会只问handle
,还有其他内容,附上大厂面试题整理的合集,这是我的学习笔记,进行了分类,循序渐进,由基础到深入,由易到简。将内容整理成了五个章节
学习PDF大全+字节跳动真题+简历模板
’计算机基础面试题、数据结构和算法面试题、Java
面试题、Android
面试题、其他扩展面试题、非技术面试题总共五个章节354页。
还有一份Android
学习PDF
大全,这份Android
学习PDF
大全真的包含了方方面面了
内含Java
基础知识点、Android
基础、Android
进阶延伸、算法合集等等
字节跳动真题解析、Android
知识大全PDF
、简历模板可以关注我看个人简介或者私信我免费获取
面试时HR
也是不可以忽略的环节,我们经常也会遇到很多关于简历制作,职业困惑、HR
经典面试问题回答等有关面试的问题。
有全套简历制作、春招困惑、HR
面试等问题解析参考建议。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。