前言
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
面試等問題解析參考建議。