這是 JsonChao 的第193期分享
前言Handler系列文章共兩篇:
第一篇:"一篇就夠"系列: Handler 消息機制完全解析
第二篇:"一篇就夠"系列: Handler 深入擴展篇
在上一篇中,我們對Handler的主體部分進行了講解,今天,我們就來學習一下Handler相關的一些擴展知識,講完這些擴展知識後,在來回答之前列出來的一系列問題。
同步屏障通過上一篇的學習,我們知道: Handler發送的Message會放入到MessageQueue中,MessageQueue中維護了一個優先級隊列,優先級隊列的意思就是將存儲數據的單鍊表按照時間升序進行排序形成的,Looper則按照順序,每次從這個優先級隊列中取出一個Message進行分發,一個處理完就處理下一個。
那麼問題來了:我能不能讓我的一個Message被優先處理?
可以,使用同步屏障
這裡,我心裡又會有個疑問,什麼是同步屏障?怎麼使用同步屏障?同步屏障有啥作用?帶着這些疑問🤔️,我們來分析下源碼
先看下MessageQueue的next方法,在上一篇中,我們省略了一部分代碼,其中有一部分是這樣子的,僅貼出關鍵代碼
Messagenext(){//...for(;;){synchronized(this){if(msg!=null&&msg.target==null){//Stalledbyabarrier.Findthenextasynchronousmessageinthequeue.do{prevMsg=msg;msg=msg.next;}while(msg!=null&&!msg.isAsynchronous());}}}上述代碼:
1、判斷當前msg不為空並且msg.target為空,則進入條件體裡面
2、條件體裡面有一行源碼注釋,翻譯過來就是: 被一個屏障給阻礙。在隊列中查找下一個異步消息
3、接下來就是一個循環,遍歷找出一條異步消息,循環體裡面就是鍊表相關的操作
這裡大家是不是會有個疑問?msg.target怎麼可能會為空呢?之前發送消息的一系列方法不是都會給msg.target對象賦值嗎?
沒錯,我們在回顧一下Handler的enqueueMessage:
privatebooleanenqueueMessage(@NonNullMessageQueuequeue,@NonNullMessagemsg,longuptimeMillis){//將當前Handler賦值給msg.targetmsg.target=this;msg.workSourceUid=ThreadLocalWorkSource.getUid();if(mAsynchronous){msg.setAsynchronous(true);}//調用MessageQueue的enqueueMessage方法returnqueue.enqueueMessage(msg,uptimeMillis);}我們知道Handler的post和send系列方法發送的消息,最終都會走到這個方法,msg.target都會被賦值,因此不可能為空。那msg.target啥時候會為空呢?我們推斷肯定是其他發送消息的方法使得msg.target為空,那我們就找一下,會發現MessageQueue的postSyncBarrier的方法中沒有給msg.target對象賦值:
publicintpostSyncBarrier(){returnpostSyncBarrier(SystemClock.uptimeMillis());}privateintpostSyncBarrier(longwhen){//Enqueueanewsyncbarriertoken.//Wedon'tneedtowakethequeuebecausethepurposeofabarrieristostallit.synchronized(this){finalinttoken=mNextBarrierToken++;finalMessagemsg=Message.obtain();msg.markInUse();msg.when=when;msg.arg1=token;Messageprev=null;Messagep=mMessages;if(when!=0){while(p!=null&&p.when<=when){prev=p;p=p.next;}}if(prev!=null){//invariant:p==prev.nextmsg.next=p;prev.next=msg;}else{msg.next=p;mMessages=msg;}returntoken;}}上述代碼就是往消息隊列中合適的位置插入target屬性為null的Message
因此我們是不是可以知道,Message的target屬性為空和非空是很不一樣的,這裡就不賣關子了,直接給結論: target屬性為空的Message就是同步屏障,他是一種特殊的消息,並不會被消費,僅僅是作為一個標識處於 MessageQueue 中,當MessageQueue的next方法遇到同步屏障的時候,就會循環遍歷整個鍊表找到標記為異步消息的Message,其他的消息會直接忽視,那麼這樣異步消息就會提前被執行了
現在我們現在就可以回答上面的問題了:target屬性為空的Message就是同步屏障,同步屏障可以使得異步消息優先被處理,通過MessageQueue的postSyncBarrier可以添加一個同步屏障
注意: 在異步消息處理完之後,同步屏障並不會被移除,需要我們手動移除,從上面的源碼我們也可以看出,如果不移除同步屏障,那麼他會一直在那裡,這樣同步消息就永遠無法被執行了。
因此我們在使用完同步屏障後,需要手動移除,代碼如下:
publicvoidremoveSyncBarrier(inttoken){//Removeasyncbarriertokenfromthequeue.//Ifthequeueisnolongerstalledbyabarrierthenwakeit.synchronized(this){Messageprev=null;Messagep=mMessages;while(p!=null&&(p.target!=null||p.arg1!=token)){prev=p;p=p.next;}if(p==null){thrownewIllegalStateException("Thespecifiedmessagequeuesynchronization"+"barriertokenhasnotbeenpostedorhasalreadybeenremoved.");}finalbooleanneedWake;if(prev!=null){prev.next=p.next;needWake=false;}else{mMessages=p.next;needWake=mMessages==null||mMessages.target!=null;}p.recycleUnchecked();//Iftheloopisquittingthenitisalreadyawake.//WecanassumemPtr!=0whenmQuittingisfalse.if(needWake&&!mQuitting){nativeWake(mPtr);}}}到這裡我心裡又有一個疑問了?怎麼把一個消息變成異步消息呢?還是回到Handler的enqueueMessage方法:
privatebooleanenqueueMessage(@NonNullMessageQueuequeue,@NonNullMessagemsg,longuptimeMillis){msg.target=this;msg.workSourceUid=ThreadLocalWorkSource.getUid();//如果mAsynchronous,則將該消息設置為異步消息if(mAsynchronous){msg.setAsynchronous(true);}returnqueue.enqueueMessage(msg,uptimeMillis);}從上述代碼我是可以看到,通過msg.setAsynchronous方法設置為true,可以把一個消息變成異步消息,但是前提得滿足mAsynchronous屬性為true,mAsynchronous是Handler中的一個屬性,他會在這兩個構造方法中被賦值:
@UnsupportedAppUsagepublicHandler(@NonNullLooperlooper,@NullableCallbackcallback,booleanasync){//...mAsynchronous=async;}publicHandler(@NullableCallbackcallback,booleanasync){//...mAsynchronous=async;}因此我們是不是可以得出結論,把一個消息設置為異步消息,有兩種方式:
1、在Handler的構造方法中,傳入async為true,那麼這個時候發送的Message就都是異步的的消息
2、給Message通過setAsynchronous 方法標誌為異步
但是,上面兩個構造方法對外是不可見的,我們調用不到,而且設置同步屏障的方法對外也是不可見的,說明谷歌不想要我們去使用他。所以這裡同步屏障也是作為一個了解,一般只有系統會去使用它,例如:在進行UI繪製的時候,以下是ViewRootImpl中執行UI繪製的方法使用到了同步屏障:
@UnsupportedAppUsagevoidscheduleTraversals(){if(!mTraversalScheduled){mTraversalScheduled=true;mTraversalBarrier=mHandler.getLooper().getQueue().postSyncBarrier();mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL,mTraversalRunnable,null);if(!mUnbufferedInputDispatch){scheduleConsumeBatchedInput();}notifyRendererOfFramePending();pokeDrawLockIfNeeded();}}voidunscheduleTraversals(){if(mTraversalScheduled){mTraversalScheduled=false;mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);mChoreographer.removeCallbacks(Choreographer.CALLBACK_TRAVERSAL,mTraversalRunnable,null);}}上述代碼在把繪製消息放入隊列之前,先放入了一個同步屏障,然後在發送異步繪製消息,從而使得界面繪製的消息會比其他消息優先執行,避免了因為 MessageQueue 中消息太多導致繪製消息被阻塞導致畫面卡頓,當繪製完成後,就會將同步屏障移除。
IdleHandler見名知意,idle是空閒的意思,那麼IdleHandler就是空閒的Handler,有點這個意思,實際上它是MessageQueue中有一個靜態接口
publicstaticinterfaceIdleHandler{booleanqueueIdle();}可以看到它是一個單方法的接口,也可稱為函數型接口,它的作用是:**在UI線程處理完所有View事務後,回調一些額外的操作,且不會堵塞主進程;**我們來實際操作一下
publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);newHandler().getLooper().getQueue().addIdleHandler(newMessageQueue.IdleHandler(){@OverridepublicbooleanqueueIdle(){Log.d("print","queueIdle:空閒時做一些輕量級別");returnfalse;}});}}//上面代碼會打印如下結果queueIdle:空閒時做一些輕量級別接着進行源碼分析,我們在看下addIdleHandler這個方法:
publicvoidaddIdleHandler(@NonNullIdleHandlerhandler){if(handler==null){thrownewNullPointerException("Can'taddanullIdleHandler");}synchronized(this){mIdleHandlers.add(handler);}}可以看到,被添加進來的handler放到了mIdleHandlers,跟過去看下mIdleHandlers,會發現MessageQueue中定義了IdleHandler的集合和數組,並且有一些操作方法,如下:
privatefinalArrayList<IdleHandler>mIdleHandlers=newArrayList<IdleHandler>();privateIdleHandler[]mPendingIdleHandlers;publicvoidaddIdleHandler(@NonNullIdleHandlerhandler){if(handler==null){thrownewNullPointerException("Can'taddanullIdleHandler");}synchronized(this){mIdleHandlers.add(handler);}}publicvoidremoveIdleHandler(@NonNullIdleHandlerhandler){synchronized(this){mIdleHandlers.remove(handler);}}最後在看下MessageQueue中的Next方法,僅貼出關鍵代碼:
Messagenext(){intpendingIdleHandlerCount=-1;//-1onlyduringfirstiterationfor(;;){//...synchronized(this){//...//當前無消息,或還需要等待一段時間消息才能分發,獲得IdleHandler的數量if(pendingIdleHandlerCount<0&&(mMessages==null||now<mMessages.when)){pendingIdleHandlerCount=mIdleHandlers.size();}if(pendingIdleHandlerCount<=0){//Noidlehandlerstorun.Loopandwaitsomemore.//如果沒有idlehandler需要執行,阻塞線程進入下次循環mBlocked=true;continue;}//初始化mPendingIdleHandlersif(mPendingIdleHandlers==null){mPendingIdleHandlers=newIdleHandler[Math.max(pendingIdleHandlerCount,4)];}//把List轉化成數組類型mPendingIdleHandlers=mIdleHandlers.toArray(mPendingIdleHandlers);}//循環遍歷所有的IdleHandlerfor(inti=0;i<pendingIdleHandlerCount;i++){finalIdleHandleridler=mPendingIdleHandlers[i];mPendingIdleHandlers[i]=null;//releasethereferencetothehandlerbooleankeep=false;try{//獲得idler.queueIdle的返回值keep=idler.queueIdle();}catch(Throwablet){Log.wtf(TAG,"IdleHandlerthrewexception",t);}//keep即idler.queueIdle的返回值,如果為false表明只要執行一次,並移除,否則不移除if(!keep){synchronized(this){mIdleHandlers.remove(idler);}}}//Resettheidlehandlercountto0sowedonotrunthemagain.//將pendingIdleHandlerCount置為0避免下次再次執行pendingIdleHandlerCount=0;//當在執行IdleHandler的時候,可能有新的消息已經進來了//所以這個時候不能阻塞,要回去循環一次看一下nextPollTimeoutMillis=0;}}上述代碼解析:
1、當調用next方法的時候,會將pendingIdleHandlerCount賦值為-1
2、判斷pendingIdleHandlerCount是否小於0並且MessageQueue 是否為空或者有延遲消息需要執行,如果是則把存儲IdleHandler的list的長度賦值給pendingIdleHandlerCount
3、判斷如果沒有IdleHandler需要執行,阻塞線程進入下次循環,如果有,則初始化mPendingIdleHandlers,把list中的所有IdleHandler放到數組中。這一步是為了不讓在執行IdleHandler的時候List被插入新的IdleHandler,造成邏輯混亂
4、循環遍歷所有的IdleHandler並執行,查看idler.queueIdle方法的返回值,為false表明這個IdleHandler只需要執行一次,並移除,為true,則不移除
5、將pendingIdleHandlerCount置為0避免下次再次執行, 當在執行IdleHandler的時候,可能有新的消息已經進來了,所以這個時候不能阻塞,要回去循環一次看一下
到這裡同步屏障和IdleHandler都講完了,建議讀者配合完整的源碼在去仔細閱讀一次。
實際應用: 可以在IdleHandler裡面獲取View的寬高
主線程消息循環在上一篇中我們講到,ActivityThread就是主線程,也可以說是UI線程,在主線程的main方法中創建了Looper,並開啟了消息循環:
publicstaticvoidmain(String[]args){//...//創建LooperLooper.prepareMainLooper();ActivityThreadthread=newActivityThread();thread.attach(false);if(sMainThreadHandler==null){sMainThreadHandler=thread.getHandler();}//...//開啟循環讀取消息Looper.loop();//Looper如果因異常原因停止循環則拋異常thrownewRuntimeException("Mainthreadloopunexpectedlyexited");}主線程的消息循環開始了以後,ActivityThread還需要有一個Handler來和消息隊列進行交互,這個Handler就是ActivityThread.H,它內部定義了很多的消息類型,例如四大組件的啟動,Application的啟動等等
classHextendsHandler{publicstaticfinalintBIND_APPLICATION=110;@UnsupportedAppUsagepublicstaticfinalintEXIT_APPLICATION=111;@UnsupportedAppUsagepublicstaticfinalintRECEIVER=113;@UnsupportedAppUsagepublicstaticfinalintCREATE_SERVICE=114;@UnsupportedAppUsagepublicstaticfinalintSERVICE_ARGS=115;@UnsupportedAppUsagepublicstaticfinalintSTOP_SERVICE=116;publicstaticfinalintCONFIGURATION_CHANGED=118;publicstaticfinalintCLEAN_UP_CONTEXT=119;@UnsupportedAppUsagepublicstaticfinalintGC_WHEN_IDLE=120;@UnsupportedAppUsagepublicstaticfinalintBIND_SERVICE=121;@UnsupportedAppUsagepublicstaticfinalintUNBIND_SERVICE=122;publicstaticfinalintDUMP_SERVICE=123;publicstaticfinalintLOW_MEMORY=124;publicstaticfinalintPROFILER_CONTROL=127;publicstaticfinalintCREATE_BACKUP_AGENT=128;publicstaticfinalintDESTROY_BACKUP_AGENT=129;publicstaticfinalintSUICIDE=130;//...publicvoidhandleMessage(Messagemsg){//...}}關於ActivityThread.H的實際應用,我們在看Activity的啟動流程可能會有比較深入的理解,ActivityThread通過ApplicationThread和AMS進行進程間通信的方式完成ActivityThread的請求後,會回調ApplicationThread中的Binder方法,然後ApplicationThread會向H發送消息,H收到消息後會將ApplicationThread中的邏輯切換到ActivityThread中去執行,即切換到主線程去執行,這個過程就是主線程的消息循環模型
妙用 Looper 機制1、我們可以通過LoopergetMainLooper方法獲取主線程Looper,從而可以判斷當前線程是否是主線程
2、將 Runnable post 到主線程執行
publicfinalclassMainThread{privateMainThread(){}privatestaticfinalHandlerHANDLER=newHandler(Looper.getMainLooper());publicstaticvoidrun(@NonNullRunnablerunnable){if(isMainThread()){runnable.run();}else{HANDLER.post(runnable);}}publicstaticbooleanisMainThread(){returnLooper.myLooper()==Looper.getMainLooper();}}子線程使用Handler及相關注意事項我們通常使用Handler都是從子線程發送消息到主線程去處理,那麼這裡我們嘗試一下從主線程發送消息到子線程來處理,上代碼:
publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//創建線程實例並開啟MyThreadmyThread=newMyThread();myThread.start();//打開這段注釋就不會crash,且看下面分析//try{//Thread.sleep(500);//}catch(InterruptedExceptione){//e.printStackTrace();//}//獲取Handler發送消息myThread.getHandler().sendEmptyMessage(0x001);}publicstaticclassMyThreadextendsThread{privateHandlermHandler;publicvoidrun(){Looper.prepare();mHandler=newHandler(){publicvoidhandleMessage(@NonNullMessagemsg){if(msg.what==0x001){Log.d("print","handleMessage:");}}};Looper.loop();}publicHandlergetHandler(){returnmHandler;}}}運行一下上述代碼,發現會Crash,如下圖:

報了一個空指針異常,原因就是多線程並發,當主線程執行到sendEnptyMessage時,子線程的Handler還沒有創建。因此我們可以在獲取Handler的時候讓主線程休眠一下在執行,應用就不會Crash了,打開上面代碼的注釋即可
值得注意的是:我們自己創建的Looper在使用完畢後應該調用quit方法來終止消息循環,如果不退出的話,那麼該線程的Looper處理完所有的消息後,就會處於一個阻塞狀態,要知道線程是比較重量級的,如果一直存在,肯定會對應用性能造成一定的影響。而如果退出Looper,這個線程就會立刻終止,因此建議不需要的時候終止Looper。
因此在子線程使用Handler,我們需要注意一下兩點:
1、必須調用Looper.prepare()創建當前線程的 Looper,並調用Looper.loop()開啟消息循環
2、必須在使用結束後調用Looper的quit方法退出當前線程
HandlerThread上面講到主線程發送消息到子線程來處理,其實Android已經給我們提供了一個這樣輕量級的異步類,那就是HandlerThread
HandlerThread的實現原理也比較簡單:繼承Thread並對Looper進行了封裝
具體源碼就不過多分析了,大家有興趣的可以去看一下,也就100多行代碼,這裡主要講解一下使用:
publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//1,創建Handler實例HandlerThreadmHandlerThread=newHandlerThread("HandlerThread");//2,啟動線程mHandlerThread.start();//3,使用傳入Looper為參數的構造方法創建Handler實例HandlermHandler=newHandler(mHandlerThread.getLooper()){@OverridepublicvoidhandleMessage(@NonNullMessagemsg){Log.d("print","當前線程:"+Thread.currentThread().getName()+"handleMessage");}};//4,使用Handler發送消息mHandler.sendEmptyMessage(0x001);//5,在合適的時機調用HandlerThread的quit方法,退出消息循環}}//上述代碼打印結果:當前線程:HandlerThreadhandleMessageHandler HandlerThread Thread三者區別Handler:在Android中負責發送和處理消息
HandlerThread:繼承自Thread,對Looper進行了封裝,也就是說它在子線程維護了一個Looper,方便我們在子線程中去處理消息
Thread: cpu執行的最小單位,即線程,它在執行完後就立馬結束了,並不能去處理消息。如果要處理,需要配合Looper,Handler一起使用
子線程彈Toast//1newThread(){@Overridepublicvoidrun(){Toast.makeText(MainActivity.this,"子線程彈Toast",Toast.LENGTH_SHORT).show();}}.start();//2newThread(){@Overridepublicvoidrun(){Looper.prepare();Toast.makeText(MainActivity.this,"子線程彈Toast",Toast.LENGTH_SHORT).show();Looper.loop();}}.start();上述1代碼運行會奔潰,會報這麼一個異常提示:"Can't toast on a thread that has not called Looper.prepare()"
原因就是Toast的實現也是依賴Handler,而我們知道在子線程中創建Handler,需先創建Looper並開啟消息循環,這點在Toast中的源碼也有體現,如下圖:

因此我們在子線程創建Toast就需要使用上述2代碼的方式
子線程彈DialognewThread(){@Overridepublicvoidrun(){Looper.prepare();newAlertDialog.Builder(MainActivity.this).setTitle("標題").setMessage("子線程彈Dialog").setNegativeButton("取消",null).setPositiveButton("確定",null).show();Looper.loop();}}.start();和上面Toast差不多,這裡貼出正確的代碼示例,它的實現也是依賴Handler,我們在它的源碼中可以看到:
privatefinalHandlermHandler=newHandler();他直接就new了一個Handler實例,我們知道,創建Handler,需要先創建Looper並開啟消息循環,主線程中已經給我們創建並開啟消息循環,而子線程中並沒有,如果不創建那就會報這句經典的異常提示:"Can't create handler inside thread that has not called Looper.prepare() ",因此在子線程中,需要我們手動去創建並開啟消息循環
到這裡,Handler相關的擴展知識就全部講完了,我們會發現也有着很多使用的小技巧,比如 IdleHandler,判斷是否是主線程等等
由於 Handler 的特性,它在 Android 里的應用非常廣泛,比如:AsyncTask、HandlerThread、Messenger、IdleHandler 和 IntentService 等等,下面我們來回答上一篇中列出來的一系列問題
問題1、Handler有哪些作用?答:
1、Handler能夠進行線程之間的切換
2、Handler能夠按照順序處理消息,避免並發
3、Handler能夠阻塞線程
4、Handler能夠發送並處理延遲消息
解析:
1、Handler能夠進行線程之間的切換,是因為使用了不同線程的Looper處理消息
2、Handler能夠按照順序處理消息,避免並發,是因為消息在入隊的時候會按照時間升序對當前鍊表進行排序,Looper讀取的時候,MessageQueue的next方法會循環加鎖,同時配合阻塞喚醒機制
3、Handler能夠阻塞線程主要是基於Linux的epoll機制實現的
4、Handler能夠處理延遲消息,是因為MessageQueue的next方法中會拿當前消息時間和當前時間做比較,如果是延遲消息,那麼就會阻塞當前線程,等阻塞時間到,在執行該消息
2、為什麼我們能在主線程直接使用Handler,而不需要創建Looper?答:主線程已經創建了Looper,並開啟了消息循環
3、如果想要在子線程創建Handler,需要做什麼準備?答:需要先創建Looper,並開啟消息循環
4、一個線程有幾個Handler?答:可以有任意多個
5、一個線程有幾個Looper?如何保證?答:一個線程只有一個Looper,通過ThreadLocal來保證
6、Handler發送消息的時候,時間為啥要取SystemClock.uptimeMillis() + delayMillis,可以把SystemClock.uptimeMillis() 換成System.currentTimeMillis()嗎?答:不可以
SystemClock.uptimeMillis() 這個方法獲取的時間,是自系統開機到現在的一個毫秒數,這個時間是個相對的
System.currentTimeMillis() 這個方法獲取的是自1970-01-01 00:00:00 到現在的一個毫秒數,這是一個和系統強關聯的時間,而且這個值可以做修改
1、使用System.currentTimeMillis()可能會導致延遲消息失效
2、最終這個時間會被設置到Message的when屬性,而Message的when屬性只是需要一個時間差來表示消息的先後順序,使用一個相對時間就行了,沒必要使用一個絕對時間
7、為什麼Looper死循環,卻不會導致應用卡死?答:Looper死循環和應用卡死是兩個不同的概念
應用卡死即ANR: 全稱Applicationn Not Responding,中文意思是應用無響應,當我發送一個消息到主線程,經過一定時間消息沒有被執行完,那麼這個時候就會拋出ANR異常
Looper死循環: 循環執行各種事務,Looper死循環說明線程還活着,如果沒有Looper死循環,線程結束,應用就退出了,當Looper處理完所有消息的時候會調用epoll機制進入阻塞狀態,當有新的Message進來的時候會打破阻塞繼續執行
8、Handler內存泄露原因? 如何解決?內存泄漏的本質是長生命周期的對象持有短生命周期對象的引用,導致短生命周期的對象無法被回收,從而導致了內存泄漏
下面我們就看個導致內存泄漏的例子
publicclassMainActivityextendsAppCompatActivity{privatefinalHandlermHandler=newHandler(){@OverridepublicvoidhandleMessage(@NonNullMessagemsg){//dosomething}};@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//發送一個延遲消息,10分鐘後在執行mHandler.sendEmptyMessageDelayed(0x001,10*60*1000);}}上述代碼:
1、我們通過匿名內部類的方式創建了一個Handler的實例
2、在onCreate方法裡面通過Handler實例發送了一個延遲10分鐘執行的消息
我們發送的這個延遲10分鐘執行的消息它是持有Handler的引用的,根據Java特性我們又知道,非靜態內部類會持有外部類的引用,因此當前Handler又持有Activity的引用,而Message又存在MessageQueue中,MessageQueue又在當前線程中,因此會存在一個引用鏈關係:
當前線程->MessageQueue->Message->Handler->Activity
因此當我們退出Activity的時候,由於消息需要在10分鐘後在執行,因此會一直持有Activity,從而導致了Activity的內存泄漏
通過上面分析我們知道了內存泄漏的原因就是持有了Activity的引用,那我們是不是會想,切斷這條引用,那麼如果我們需要用到Activity相關的屬性和方法採用弱引用的方式不就可以了麼?我們實際操作一下,把Handler寫成一個靜態內部類
publicclassMainActivityextendsAppCompatActivity{privatefinalSafeHandlermSafeHandler=newSafeHandler(this);@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//發送一個延遲消息,10分鐘後在執行mSafeHandler.sendEmptyMessageDelayed(0x001,10*60*1000);}//靜態內部類並持有Activity的弱引用privatestaticclassSafeHandlerextendsHandler{privatefinalWeakReference<MainActivity>mWeakReference;publicSafeHandler(MainActivityactivity){mWeakReference=newWeakReference<>(activity);}@OverridepublicvoidhandleMessage(@NonNullMessagemsg){MainActivitymMainActivity=mWeakReference.get();if(mMainActivity!=null){//dosomething}}}}上述代碼
1、把Handler定義成了一個靜態內部類,並持有當前Activity的弱引用,弱引用會在Java虛擬機發生gc的時候把對象給回收掉
經過上述改造,我們解決了Activity的內存泄漏,此時的引用鏈關係為:
當前線程->MessageQueue->Message->Handler
我們會發現Message還是會持有Handler的引用,從而導致Handler也會內存泄漏,所以我們應該在Activity銷毀的時候,在他的生命周期方法裡,把MessageQueue中的Message都給移除掉,因此最終就變成了這樣:
publicclassMainActivityextendsAppCompatActivity{privatefinalSafeHandlermSafeHandler=newSafeHandler(this);@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//發送一個延遲消息,10分鐘後在執行mSafeHandler.sendEmptyMessageDelayed(0x001,10*60*1000);}@OverrideprotectedvoidonDestroy(){super.onDestroy();mSafeHandler.removeCallbacksAndMessages(null);}//靜態內部類並持有Activity的弱引用privatestaticclassSafeHandlerextendsHandler{privatefinalWeakReference<MainActivity>mWeakReference;publicSafeHandler(MainActivityactivity){mWeakReference=newWeakReference<>(activity);}@OverridepublicvoidhandleMessage(@NonNullMessagemsg){MainActivitymMainActivity=mWeakReference.get();if(mMainActivity!=null){//dosomething}}}}因此當Activity銷毀後,引用鏈關係為:
當前線程->MessageQueue
而當前線程和MessageQueue的生命周期和應用生命周期是一樣長的,因此也就不存在內存泄漏了,完美。
所以解決Handler內存泄漏最好的方式就是:將Handler定義成靜態內部類,內部持有Activity的弱引用,並在Activity銷毀的時候移除所有消息
9、線程維護的Looper,在消息隊列無消息時的處理方案是什麼?有什麼用?答:當消息隊列無消息時,Looper會阻塞當前線程,釋放cpu資源,提高App性能
我們知道Looper的loop方法中有個死循環一直在讀取MessageQueue中的消息,其實是調用了MessageQueue中的next方法,這個方法會在無消息時,調用Linux的epoll機制,使得線程進入阻塞狀態,當有新消息到來時,就會將它喚醒,next方法裡會判斷當前消息是否是延遲消息,如果是則阻塞線程,如果不是,則會返回這條消息並將其從優先級隊列中給移除
10、MessageQueue什麼情況下會被喚醒?答:需要分情況
1、發送消息過來,此時MessageQueue中無消息或者當前發送過來的消息攜帶的when為0或者有延遲執行的消息,那麼需要喚醒
2、當遇到同步屏障且當前發送過來的消息為異步消息,判斷該異步消息是否插入在所有異步消息的隊首,如果是則需要喚醒,如果不是,則不喚醒
11、線程什麼情況下會被阻塞?答:分情況
1、當MessageQueue中沒有消息的時候,這個時候會無限阻塞,
2、當前MessageQueue中全部是延遲消息,阻塞時間為(當前延遲消息時間 - 當前時間),如果這個阻塞時間超過來Integer類型的最大值,則取Integer類型的最大值
12、我們可以使用多個Handler往消息隊列中添加數據,那麼可能存在發消息的Handler存在不同的線程,那麼Handler是如何保證MessageQueue並發訪問安全的呢?答:循環加鎖,配合阻塞喚醒機制
我們可以發現MessageQueue其實是「生產者-消費者」模型,Handler不斷地放入消息,Looper不斷地取出,這就涉及到死鎖問題。如果Looper拿到鎖,但是隊列中沒有消息,就會一直等待,而Handler需要把消息放進去,鎖卻被Looper拿着無法入隊,這就造成了死鎖。Handler機制的解決方法是循環加鎖。在MessageQueue的next方法中:
Messagenext(){...for(;;){...nativePollOnce(ptr,nextPollTimeoutMillis);synchronized(this){...}}}我們可以看到他的等待是在鎖外的,當隊列中沒有消息的時候,他會先釋放鎖,再進行等待,直到被喚醒。這樣就不會造成死鎖問題了。
那在入隊的時候會不會因為隊列已經滿了然後一邊在等待消息處理一邊拿着鎖呢?這一點不同的是MessageQueue的消息沒有上限,或者說他的上限就是JVM給程序分配的內存,如果超出內存會拋出異常,但一般情況下是不會的。
13、Handler是如何進行線程切換的呢?答:使用不同線程的Looper處理消息
我們通常處理消息是在Handler的handleMessage方法中,那麼這個方法是在哪裡回調的呢?看下面這段代碼
publicstaticvoidloop(){//開啟死循環讀取消息for(;;){//調用Message對應的Handler處理消息msg.target.dispatchMessage(msg);}}上述代碼中msg.target其實就是我們發送消息的Handler,因此他會回調Handler的dispatchMessage方法,而dispatchMessage這個方法我們在上一篇中重點分析過,其中有一部分邏輯就是會回調到Handler的handleMessage方法,我們還可以發現,Handler的handleMessage方法所在的線程是由Looper的loop方法決定的。平時我們使用的時候,是從異步線程發送消息到 Handler,而這個 Handler 的 handleMessage() 方法是在主線程調用的,因為Looper是在主線程創建的,所以消息就從異步線程切換到了主線程。
14、我們在使用Message的時候,應該如何去創建它?答:Android 給 Message 設計了回收機制,官方建議是通過Message.obtain方法來獲取,而不是直接new一個新的對象,所以我們在使用的時候應儘量復用 Message ,減少內存消耗,方式有二:
1、調用 Message 的一系列靜態重載方法 Message.obtain 獲取
2、通過 Handler 的公有方法 handler.obtainMessage,實際上handler.obtainMessage內部調用的也是Message.obtain的重載方法
15、Handler裡面藏着的CallBack能做什麼?答: 利用此CallBack攔截Handler的消息處理
在上一篇中我們分析到,dispatchMessage方法的處理步驟:
1、首先,檢查Message的callback是否為null,不為null就通過handleCallBack來處理消息,Message的callback是一個Runnable對象,實際上就是Handler的post系列方法所傳遞的Runnable參數
2、其次,檢查Handler裡面藏着的CallBack是否為null,不為null就調用mCallback的handleMessage方法來處理消息,並判斷其返回值:為true,那麼 Handler 的 handleMessage(msg) 方法就不會被調用了;為false,那麼就意味着一個消息可以同時被 Callback 以及 Handler 處理。
3、最後,調用Handler的handleMessage方法來處理消息
通過上面分析我們知道Handler處理消息的順序是:Message的Callback > Handler的Callback > Handler的handleMessage方法
使用場景: Hook ActivityThread.mH , 在 ActivityThread 中有個成員變量 mH ,它是個 Handler,又是個極其重要的類,幾乎所有的插件化框架都使用了這個方法。
16、Handler阻塞喚醒機制是怎麼一回事?答:Handler的阻塞喚醒機制是基於Linux的阻塞喚醒機制。
這個機制也是類似於handler機制的模式。在本地創建一個文件描述符,然後需要等待的一方則監聽這個文件描述符,喚醒的一方只需要修改這個文件,那麼等待的一方就會收到文件從而打破喚醒。和Looper監聽MessageQueue,Handler添加message是比較類似的。具體的Linux層知識讀者可通過這篇文章詳細了解(傳送門)
17、什麼是Handler的同步屏障?答: 同步屏障是一種使得異步消息可以被更快處理的機制
18、能不能讓一個Message被加急處理?答:可以,添加加同步屏障,並發送異步消息
19、什麼是IdleHandler?答: IdleHandler是MessageQueue中一個靜態函數型接口,它在主線程執行完所有的View事務後,回調一些額外的操作,且不會阻塞主線程
總結Handler消息機制在Android系統源碼中進行了大量的使用,可以說是涉及了Android的方方面面,比如我們四大組件的啟動,Application的創建等等,學好Handler相關的知識,可以幫助我們更好的去閱讀Android源碼,而且Handler在我們日常開發中直接或間接的會被用到。同時通過對Handler源碼的學習,讓我感受到了代碼設計的背後,蘊藏着工程師大量的智慧,心裡直呼666,哈哈。
到了這裡,關於Handler相關的知識就都講完了,如果你還有什麼問題,評論區告訴我吧。
END
參考鏈接:
1、Android全面解析之Handler機制(終篇):常見問題匯總
2、Handler 都沒搞懂,拿什麼去跳槽啊?
3、換個姿勢,帶着問題看Handler
往期推薦
這兩年,我打造了一份具備競爭壁壘的 Android 性能優化 通關秘籍
三個值得深入思考的 Android 問答分享(第 3 期)
這兩年,我打造了一份具備競爭壁壘的 NDK 通關秘籍
三個值得深入思考的 Android 問答分享(第 2 期)
這兩年,我打造了一份具備競爭壁壘的 Android 基礎架構 通關秘籍
點擊下方卡片關注JsonChao,為你構建一套
未來技術人必備的底層能力系統
▲點擊上方卡片關注JsonChao,構建一套
未來Android開發必備的知識體系
歡迎把文章分享到朋友圈