不想被面試官虐?Android知識彙總,你必須知道的Handler八大問題!
前言
handler
機制幾乎是
Android
面試時必問的問題,雖然看過很多次
handler
原始碼,但是有些面試官問的問題卻不一定能夠回答出來,趁著機會總結一下面試中所覆蓋的
handler
知識點。
1、講講 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
中的訊息都在同一條執行緒中執行。
2、Handler機制,sendMessage和post(Runnable)的區別?
要看
sendMessage
和
post
區別,需要從原始碼來看,下面是幾種使用
handler
的方式,先看下這些方式,然後再從原始碼分析有什麼區別。
例1、 主執行緒中使用
handler
//主執行緒 Handler mHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(@NonNull Message msg) { if (msg.what == 1) { //doing something } return false; } }); Message msg = Message.obtain(); msg.what = 1; mHandler.sendMessage(msg);
上面是在主執行緒中使用
handler
,因為在
Android
中系統已經在主執行緒中生成了
Looper
,所以不需要自己來進行
looper
的生成。如果上面的程式碼在子執行緒中執行,就會報
Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()
如果想著子執行緒中處理
handler
的操作,就要必須要自己生成
Looper
了。
例2 、子執行緒中使用handler
Thread thread=new Thread(new Runnable() { @Override public void run() { Looper.prepare(); Handler handler=new Handler(); handler.post(new Runnable() { @Override public void run() { } }); Looper.loop(); } });
上面在
Thread
中使用
handler
,先執行
Looper.prepare
方法,來在當前執行緒中生成一個
Looper
物件並儲存在當前執行緒的
ThreadLocal
中。 看下
Looper.prepare()
中的原始碼:
//prepare private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); } //Looper private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
可以看到
prepare
方法中會先從
sThreadLocal
中取如果之前已經生成過
Looper
就會報錯,否則就會生成一個新的
Looper
並且儲存線上程的
ThreadLocal
中,這樣可以確保每一個執行緒中只能有一個唯一的
Looper
。
另外:由於
Looper
中擁有當前執行緒的引用,所以有時候可以用
Looper
的這種特點來判斷當前執行緒是不是主執行緒。
@RequiresApi(api = Build.VERSION_CODES.KITKAT) boolean isMainThread() { return Objects.requireNonNull(Looper.myLooper()).getThread() == Looper.getMainLooper().getThread(); }
sendMessage vs post
先來看看
sendMessage
的程式碼呼叫鏈:
enqueueMessage
原始碼如下:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); return queue.enqueueMessage(msg, uptimeMillis); }
enqueueMessage
的程式碼處理很簡單,
msg.target = this;
就是把當前的
handler
物件給
message.target
。然後再講
message
進入到佇列中。
post程式碼呼叫鏈:
呼叫
post
時候會先呼叫
getPostMessage
生成一個
Message
,後面和
sendMessage
的流程一樣。下面看下
getPostMessage
方法的原始碼:
private static Message getPostMessage(Runnable r) { Message m = Message.obtain(); m.callback = r; return m; }
可以看到
getPostMessage
中會先生成一個
Messgae
,並且把
runnable
賦值給
message
的
callback.
訊息都放到
MessageQueue
中後,看下
Looper
是如何處理的。
for (;;) { Message msg = queue.next(); // might block if (msg == null) { return; } msg.target.dispatchMessage(msg); }
Looper
中會遍歷
message
列表,當
message
不為
null
時呼叫
msg.target.dispatchMessage(msg)
方法。看下
message
結構:
也就是說
msg.target.dispatchMessage
方法其實就是呼叫的
Handler中的dispatchMessage
方法,下面看下
dispatchMessage
方法的原始碼:
public void dispatchMessage(@NonNull Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } } // private static void handleCallback(Message message) { 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
方法來處理。
3、Looper會一直消耗系統資源嗎?
首先給出結論,
Looper
不會一直消耗系統資源,當
Looper
的
MessageQueue
中沒有訊息時,或者定時訊息沒到執行時間時,當前持有
Looper
的執行緒就會進入阻塞狀態。
下面看下
looper
所在的執行緒是如何進入阻塞狀態的。
looper
阻塞肯定跟訊息出隊有關,因此看下訊息出隊的程式碼。
訊息出隊
Message next() { // Return here if the message loop has already quit and been disposed. // This can happen if the application tries to restart a looper after quit // which is not supported. final long ptr = mPtr; if (ptr == 0) { return null; } int nextPollTimeoutMillis = 0; for (;;) { if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); } nativePollOnce(ptr, nextPollTimeoutMillis); // While calling an idle handler, a new message could have been delivered // so go back and look again for a pending message without waiting. if(hasNoMessage) { nextPollTimeoutMillis =-1; } } }
上面的訊息出隊方法被簡寫了,主要看下面這段,沒有訊息的時候
nextPollTimeoutMillis=-1
;
if(hasNoMessage) { nextPollTimeoutMillis =-1; }
看for迴圈裡面這個欄位所其的作用:
if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); } nativePollOnce(ptr, nextPollTimeoutMillis);
Binder.flushPendingCommands();
這個方法的作用可以看原始碼裡面給出的解釋:
/** * Flush any Binder commands pending in the current thread to the kernel * driver. This can be * useful to call before performing an operation that may block for a long * time, to ensure that any pending object references have been released * in order to prevent the process from holding on to objects longer than * it needs to. */
也就是說在使用者執行緒要進入阻塞之前跟核心執行緒傳送訊息,防止使用者執行緒長時間的持有某個物件。再看看下面這個方法:
nativePollOnce(ptr, nextPollTimeoutMillis);
當
nextPollingTimeOutMillis=-1
時,這個
native
方法會阻塞當前執行緒,執行緒阻塞後,等下次有訊息入隊才會重新進入可執行狀態,所以
Looper
並不會一直死迴圈消耗執行記憶體,對佇列中的顏色訊息還沒到時間時也會阻塞當前執行緒,但是會有一個阻塞時間也就是
nextPollingTimeOutMillis>0
的時間。
當訊息佇列中沒有訊息的時候looper肯定是被訊息入隊喚醒的。
訊息入隊
boolean enqueueMessage(Message msg, long when) { if (msg.target == null) { throw new IllegalArgumentException("Message must have a target."); } if (msg.isInUse()) { throw new IllegalStateException(msg + " This message is already in use."); } synchronized (this) { if (mQuitting) { IllegalStateException e = new IllegalStateException( msg.target + " sending message to a Handler on a dead thread"); Log.w(TAG, e.getMessage(), e); msg.recycle(); return false; } msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake; if (p == null || when == 0 || when < p.when) { // New head, wake up the event queue if blocked. msg.next = p; mMessages = msg; needWake = mBlocked; } else { // Inserted within the middle of the queue. Usually we don't have to wake // up the event queue unless there is a barrier at the head of the queue // and the message is the earliest asynchronous message in the queue. needWake = mBlocked && p.target == null && msg.isAsynchronous(); Message prev; 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.next prev.next = msg; } // We can assume mPtr != 0 because mQuitting is false. if (needWake) { nativeWake(mPtr); } } return true; }
上面可以看到訊息入隊之後會有一個
if (needWake) { nativeWake(mPtr); }
方法,呼叫這個方法就可以喚醒執行緒了。另外訊息入隊的時候是根據訊息的
delay
時間來在連結串列中排序的,
delay
時間長的排在後面,時間短的排在前面。如果時間相同那麼按插入時間先後來排,插入時間早的在前面,插入時間晚的在後面。
4、android的Handle機制,Looper關係,主執行緒的Handler是怎麼判斷收到的訊息是哪個Handler傳來的?
Looper
是如何判斷
Message
是從哪個
handler
傳來的呢?其實很簡單,在
1
中分析過,
handler
在
sendMessage
的時候會構建一個
Message
物件,並且把自己放在
Message
的
target
裡面,這樣的話
Looper
就可以根據
Message
中的
target
來判斷當前的訊息是哪個
handler
傳來的。
5、Handler機制流程、Looper中延遲訊息誰來喚醒Looper?
從3中知道在訊息出隊的
for
迴圈佇列中會呼叫到下面的方法。
nativePollOnce(ptr, nextPollTimeoutMillis);
如果是延時訊息,會在被阻塞
nextPollTimeoutMillis
時間後被叫醒,
nextPollTimeoutMillis
就是訊息要執行的時間和當前的時間差。
6、Handler是如何引起記憶體洩漏的?如何解決?
在子執行緒中,如果手動為其建立
Looper
,那麼在所有的事情完成以後應該呼叫
quit
方法來終止訊息迴圈,否則這個子執行緒就會一直處於等待的狀態,而如果退出
Looper
以後,這個執行緒就會立刻終止,因此建議不需要的時候終止
Looper
。
Looper.myLooper().quit()
那麼,如果在
Handler
的
handleMessage
方法中(或者是run方法)處理訊息,如果這個是一個延時訊息,會一直儲存在主執行緒的訊息佇列裡,並且會影響系統對
Activity
的回收,造成記憶體洩露。
具體可以參考
Handler
記憶體洩漏分析及解決
總結一下,解決
Handler
記憶體洩露主要2點
1 、有延時訊息,要在
Activity
銷燬的時候移除
Messages
2、 匿名內部類導致的洩露改為匿名靜態內部類,並且對上下文或者
Activity
使用弱引用。
7、handler機制中如何確保Looper的唯一性?
Looper
是儲存線上程的
ThreadLocal
裡面的,使用
Handler
的時候要呼叫
Looper.prepare()
來建立一個
Looper
並放在當前的執行緒的
ThreadLocal
裡面。
private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); }
可以看到,如果多次呼叫
prepare
的時候就會報
Only one Looper may be created per thread
,所以這樣就可以保證一個執行緒中只有唯一的一個
Looper
。
8、Handler 是如何能夠執行緒切換,傳送Message的?
handler
的執行跟建立
handler
的執行緒無關,跟建立
looper
的執行緒相關,加入在子執行緒中建立一個
Handler
,但是
Handler
相關的
Looper
是主執行緒的,這樣,如果
handler
執行
post
一個
runnable
,或者
sendMessage
,最終的
handle Message
都是在主執行緒中執行的。
Thread thread=new Thread(new Runnable() { @Override public void run() { Looper.prepare(); Handler handler=new Handler(getMainLooper()); handler.post(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this,"hello,world",Toast.LENGTH_LONG).show(); } }); Looper.loop(); } }); thread.start();
心裡話
不論是什麼樣的大小面試,要想不被面試官虐的不要不要的,只有刷爆面試題題做好全面的準備,除了這個還需要在平時把自己的基礎打紮實,這樣不論面試官怎麼樣一個知識點裡往死裡鑿,你也能應付如流啊~
如果文字版的
handle
彙總還有些不懂得話,我給大家準備了三星架構師講解的
2
小時影片,
Handler
面試需要的所有知識都在這,可以好好學一學!
當然,面試的時候肯定不會只問
handle
,還有其他內容,附上大廠面試題整理的合集,這是我的學習筆記,進行了分類,循序漸進,由基礎到深入,由易到簡。將內容整理成了五個章節
’
計算機基礎面試題、資料結構和演算法面試題、
Java
面試題、
Android
面試題、其他擴充套件面試題、非技術面試題總共五個章節354頁。
還有一份
Android
學習
PDF
大全,這份
Android
學習
PDF
大全真的包含了方方面面了
內含
Java
基礎知識點、
Android
基礎、
Android
進階延伸、演算法合集等等
位元組跳動真題解析、
Android
知識大全
PDF
、簡歷模板可以關注我看個人簡介或者
私信我免費獲取
面試時
HR
也是不可以忽略的環節,我們經常也會遇到很多關於簡歷製作,職業困惑、
HR
經典面試問題回答等有關面試的問題。
有全套簡歷製作、春招困惑、
HR
面試等問題解析參考建議。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69952849/viewspace-2685088/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 面試官帶你學Android——面試中Handler 這些必備知識點你都知道嗎?面試Android
- 聽說你又被面試官虐了?面試
- Android中handler問題彙總Android
- 你必須知道的Java基礎知識Java
- Android進階知識樹——Android 多程式、Binder 你必須知道的一切Android
- 關於索引必須知道的知識索引
- 關於JVM,你必須知道的這些知識點JVM
- 千萬淘寶店運營必備知識,你必須知道!
- 面試官: 我必問的容器知識點!面試
- 關於Mysql事務,你必須知道的幾個知識點!MySql
- 關於Flutter 您必須知道的知識點!!!Flutter
- 大學期間必須知道的JVM知識JVM
- Android 之 Notification 必須掌握知識點Android
- 你必須知道的 SmartSql !SQL
- JAVA高階面試必過知識點彙總Java面試
- 做iOS自動化測試必須知道的一些知識iOS
- 初學Python必須知道的11個知識點!Python
- Python入門必須知道的11個知識點Python
- 程式猿必須知道的關於 Tomcat 的知識點Tomcat
- 面試前必須要知道的Redis面試題Redis面試題
- 軟體工程師必須知道20個知識點你瞭解多少?軟體工程工程師
- 【分享】Postman介面測試實戰分享,這5個問題你必須得知道!Postman
- 50個你必須瞭解的Kubernetes面試問題面試
- 前端進階必備知識彙總前端
- Web前端必備-Nginx知識彙總Web前端Nginx
- 想要學習Python課程,這些問題你必須知道!Python
- 【docker基礎知識】docker坑問題彙總Docker
- Android的細節知識彙總系列(一)Android
- 必須懂的mysql知識MySql
- PHP面試經常被問到的知識點彙總,對你非常有用PHP面試
- 多執行緒面試必備基礎知識彙總執行緒面試
- New的幾個問題 詳解 --讀你必須知道的.NET筆記筆記
- 對於MySQL你必須要了解的鎖知識MySql
- ?你必須知道的Java泛型Java泛型
- 7款滲透測試工具,你必須知道!
- 開發者測試:你必須知道 7 件事
- OPPO R17常見問題彙總 入手OPPO R17和R17 Pro你必須知道的9件事
- 04.關於執行緒你必須知道的8個問題(下)執行緒