2018年8月1日以前謝絕全文轉(zhuǎn)載(已授權(quán)除外)
本文作者:@怪盜kidou
本文鏈接:http://www.reibang.com/p/f70ee1765a61
周末在家有點(diǎn)兒無聊,不知道該干些啥凰盔,想了想開通博客這么長時(shí)間以來好像并沒有些什么關(guān)于 Android 的東西不瓶,所以這次來寫寫Android 相關(guān)的博客 —— Handler。
為什么寫 Handler
確實(shí) Handler
是 Android
開發(fā)過程中非常非常常見的東西照瘾,講Handler的博客也不勝枚舉為什么我還要寫關(guān)于Handler的內(nèi)容?
起因是這樣的丧慈,公司為了擴(kuò)張業(yè)務(wù)準(zhǔn)備做一個(gè)新的產(chǎn)品線所以給移動(dòng)端這邊分配了4個(gè)招聘名額(iOS和Android各兩名)析命,頭一個(gè)星期我因?yàn)樵诿χ鲂枨蟛]有參與公司的面試,都是公司的另外兩個(gè)同事在參與面試逃默,后一個(gè)星期我也參與到其中鹃愤,但是我發(fā)現(xiàn)一個(gè)很嚴(yán)重的問題:在我面試的幾個(gè)人雖然工作經(jīng)驗(yàn)都集中3~6年但都沒有把 Handler
講清楚。
與其他的博客有什么不同
市面上有太多講 Handler 的博客了完域,那我的博客要如何做到讓人耳目一新并且切實(shí)能讓大家受益呢软吐?
我想了一下,Handler的基本原理很簡單吟税,但細(xì)節(jié)還是蠻多的凹耙,這次發(fā)現(xiàn)問題也是通過面試得出的,所以我決定通過模擬面試的方式告訴你關(guān)于 Handler 的那些事兒肠仪。
約定
本文的各個(gè)問題只是我自己想出來的使兔,并不是出自真實(shí)的面試中(除了部分我面試別人時(shí)的提問),其他的均為我為了給大家介紹 Handler 機(jī)制時(shí)想出的問題藤韵。
本文后面會出現(xiàn)的部分源碼,為避免小伙伴們在 Android Studio 中看到代碼與我博客中的不一致熊经,這里先統(tǒng)一一下環(huán)境:
- sdk版本:API 27
android{
compileSdkVersion 27
// ......
}
- 源碼版本:27_r03
Q:說一下 Handler機(jī)制中涉及到那些類泽艘,各自的功能是什么
A:Handler
、Looper
镐依、MessageQueue
匹涮、Message
,主要是這4個(gè)槐壳,ThreadLocal
可以不算在里面畢竟這個(gè)是JDK本身自帶類不是專門為Handler機(jī)制設(shè)計(jì)的然低。
Handler
的作用是將 Message
對象發(fā)送到 MessageQueue
中去,同時(shí)將自己的引用賦值給 Message#target
。
Looper
的作用是將 Message
對象從 MessageQueue
中取出來雳攘,并將其交給 Handler#dispatchMessage(Message)
方法带兜,這里需要主要的是:不是調(diào)用 Handler#handleMessage(Message)
方法,具體原因后面會講吨灭。
MessageQueue
的作用負(fù)責(zé)插入和取出 Message
Q:Handler 有哪些發(fā)送消息的方法
我主要是看其知不知道 post 相關(guān)的方法刚照,問了兩個(gè)人兩人都不知道有post方法
sendMessage(Message msg)
sendMessageDelayed(Message msg, long uptimeMillis)
post(Runnable r)
postDelayed(Runnable r, long uptimeMillis)
sendMessageAtTime(Message msg,long when)
下面的幾個(gè)方法在我眼中可能并不是那么重要
sendEmptyMessage(int what)
sendEmptyMessageDelayed(int what, long uptimeMillis)
sendEmptyMessageAtTime(int what, long when)
Q:MessageQueue 中的 Message 是有序的嗎?排序的依據(jù)是什么
是有序的喧兄。你可能會想這不是廢話嘛无畔,Queue
都是有序的,Set
才是無序的吠冤,這里想問你的是他的內(nèi)部是基于什么進(jìn)行的排序浑彰,排序的依據(jù)是 Message#when
字段,表示一個(gè)相對時(shí)間拯辙,該值是由 MessageQueue#enqueueMessage(Message, Long)
方法設(shè)置的郭变。
// 見 MessageQueue.java:554,566~578
boolean enqueueMessage(Message msg, long when) {
// ....
synchronized (this) {
// ....
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
// 一致循環(huán),直到找到尾巴(p == null)
// 或者這個(gè) message 的 when 小于我們當(dāng)前這個(gè) message 的 when
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
}
return true;
}
如果當(dāng)前插入的 message#when
是介于 5~8 之間薄风,那么for 循環(huán)結(jié)束時(shí) prev
和p
指向的樣子應(yīng)該是下圖的
由于這個(gè)特性饵较,所以當(dāng)兩個(gè) Message#when
一致時(shí)插入序按先后順序,比如兩個(gè)的 when 都是7遭赂,那么第一個(gè)進(jìn)入后的樣子如下圖(黃):
第二個(gè)進(jìn)入后的樣子(紅):
Q:Message#when 是指的是什么
Message#when
是一個(gè)時(shí)間循诉,用于表示 Message
期望被分發(fā)的時(shí)間,該值是 SystemClock#uptimeMillis()
與 delayMillis
之和撇他。
// Handler.java:596
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
SystemClock#uptimeMillis()
是一個(gè)表示當(dāng)前時(shí)間的一個(gè)相對時(shí)間茄猫,它代表的是 自系統(tǒng)啟動(dòng)開始從0開始的到調(diào)用該方法時(shí)相差的毫秒數(shù) 。
Q:Message#when 為什么不用 System.currentTimeMillis() 來表示
System.currentTimeMillis()
代表的是從 1970-01-01 00:00:00 到當(dāng)前時(shí)間的毫秒數(shù)困肩,這個(gè)值是一個(gè)強(qiáng)關(guān)聯(lián) 系統(tǒng)時(shí)間 的值划纽,我們可以通過修改系統(tǒng)時(shí)間達(dá)到修改該值的目的,所以該值是不可靠的值锌畸。
比如手機(jī)長時(shí)間沒有開機(jī)勇劣,開機(jī)后系統(tǒng)時(shí)間重置為出廠時(shí)設(shè)置的時(shí)間,中間我們發(fā)送了一個(gè)延遲消息潭枣,過了一段時(shí)間通過 NTP 同步了最新時(shí)間比默,那么就會導(dǎo)致 延遲消息失效
同時(shí) Message#when
只是用 時(shí)間差 來表示先后關(guān)系,所以只需要一個(gè)相對時(shí)間就可以達(dá)成目的盆犁,它可以是從系統(tǒng)啟動(dòng)開始計(jì)時(shí)的命咐,也可以是從APP啟動(dòng)時(shí)開始計(jì)時(shí)的,甚至可以是定期重置的(所有消息都減去同一個(gè)值谐岁,不過這樣就復(fù)雜了沒有必要)醋奠。
Q:子線程中可以創(chuàng)建 Handler 對象嗎榛臼?
不可以在子線程中直接調(diào)用 Handler 的無參構(gòu)造方法,因?yàn)?Handler
在創(chuàng)建時(shí)必須要綁定一個(gè) Looper
對象窜司,有兩種方法綁定
- 先調(diào)用 Looper.prepare() 在當(dāng)前線程初始化一個(gè) Looper
Looper.prepare();
Handler handler = new Handler();
// ....
// 這一步可別可少了
Looper.loop();
- 通過構(gòu)造方法傳入一個(gè) Looper
Looper looper = .....;
Handler handler = new Handler(looper);
Q:Handler 是如何與 Looper 關(guān)聯(lián)的
上個(gè)問題應(yīng)該告知了其中一種情況:通過構(gòu)造方法傳參沛善。
還有一種是我們直接調(diào)用無參構(gòu)造方法時(shí)會有一個(gè)自動(dòng)綁定過程
// Handler.java:192
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
mLooper = Looper.myLooper(); // 就是這里
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
Q:Looper 是如何與 Thread 關(guān)聯(lián)的
Looper 與 Thread 之間是通過 ThreadLocal 關(guān)聯(lián)的,這個(gè)可以看 Looper#prepare()
方法
// Looper.java:93
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
中有一個(gè) ThreadLocal
類型的 sThreadLocal
靜態(tài)字段例证,Looper
通過它的 get
和 set
方法來賦值和取值路呜。
由于 ThreadLocal
是與線程綁定的,所以我們只要把 Looper
與 ThreadLocal
綁定了织咧,那 Looper
和 Thread
也就關(guān)聯(lián)上了
ThreadLocal
的原理在問 Handler
機(jī)制的時(shí)候也是一個(gè)比較常問的點(diǎn)胀葱,但是介紹的博客很多,源碼也沒有多少笙蒙,這里就不再介紹了抵屿,如果有需要的話后期會寫新博客。
Q:Handler 有哪些構(gòu)造方法
如果你上面的問題 子線程中可以創(chuàng)建 Handler 對象嗎 沒有答上的話捅位,我一般會通過這個(gè)問題引導(dǎo)一下轧葛。
問這個(gè)問題主要是想問你構(gòu)造方法可以傳那些參數(shù),并不是要你完全說出來艇搀,但是當(dāng)你知道可以傳哪些參數(shù)的時(shí)候尿扯,也可以推算出來有幾個(gè)構(gòu)造方法。
先說可以傳那些類型(僅限開放API焰雕,被 @hide 標(biāo)注的不算在內(nèi))衷笋,僅兩種類型:Looper
、Handler$Callback
矩屁,那么我們就可以退算出有多少個(gè)公共構(gòu)造方法了:無參辟宗、單Looper、單Callback吝秕、Looper和Handler泊脐,共4種。
public Handler() {
this(null, false);
}
public Handler(Callback callback) {
this(callback, false);
}
public Handler(Looper looper) {
this(looper, null, false);
}
public Handler(Looper looper, Callback callback) {
this(looper, callback, false);
}
還有一個(gè) boolean
的 async, 不過這個(gè)不是開放API烁峭,即使不知道個(gè)人覺得完全沒有問題容客。
Q:looper為什么調(diào)用的是Handler的dispatchMessage方法
看一下dispatchMessage方法
// Handler.java:97
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
從上面的代碼不難看出有兩個(gè)原因:
- 當(dāng)
msg.callback != null
時(shí)會執(zhí)行handleCallback(msg)
,這表示這個(gè) msg 對象是通過handler#postAtTime(Runnable, long)
相關(guān)方法發(fā)送的约郁,所以msg.what
和msg.obj
都是零值缩挑,不會交給Handler#handleMessage
方法。 - 從上一個(gè)問題你應(yīng)該看到了
Handler
可以接受一個(gè)Callback
參數(shù)棍现,類似于 View 里的 OnTouchListener ,會先把事件交給Callback#handleMessage(Message)
處理镜遣,如果返回 false 時(shí)該消息才會交給Handler#handleMessage(Message)
方法己肮。
Q:在子線程中如何獲取當(dāng)前線程的 Looper
Looper.myLooper()
內(nèi)部原理就是同過上面提到的 sThreadLocal#get()
來獲取 Looper
// Looper.java:203
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
Q:如果在任意線程獲取主線程的 Looper
Looper.getMainLooper()
這個(gè)在我們開發(fā) library 時(shí)特別有用士袄,畢竟你不知道別人在調(diào)用使用你的庫時(shí)會在哪個(gè)線程初始化,所以我們在創(chuàng)建 Handler
時(shí)每次都通過指定主線程的 Looper
的方式保證庫的正常運(yùn)行谎僻。
Q:如何判斷當(dāng)前線程是不是主線程
知道了上面兩個(gè)問題娄柳,這個(gè)問題就好回答了
方法一:
Looper.myLooper() == Looper.getMainLooper()
方法二:
Looper.getMainLooper().getThread() == Thread.currentThread()
方法三: 方法二的簡化版
Looper.getMainLooper().isCurrentThread()
Q:Looper.loop() 會退出嗎?
不會自動(dòng)退出艘绍,但是我們可以通過 Looper#quit()
或者 Looper#quitSafely()
讓它退出赤拒。
兩個(gè)方法都是調(diào)用了 MessageQueue#quit(boolean)
方法,當(dāng) MessageQueue#next()
方法發(fā)現(xiàn)已經(jīng)調(diào)用過 MessageQueue#quit(boolean)
時(shí)會 return null
結(jié)束當(dāng)前調(diào)用诱鞠,否則的話即使 MessageQueue
已經(jīng)是空的了也會阻塞等待挎挖。
Q:MessageQueue#next 在沒有消息的時(shí)候會阻塞,如何恢復(fù)航夺?
當(dāng)其他線程調(diào)用 MessageQueue#enqueueMessage
時(shí)會喚醒 MessageQueue
蕉朵,這個(gè)方法會被 Handler#sendMessage
、Handler#post
等一系列發(fā)送消息的方法調(diào)用阳掐。
boolean enqueueMessage(Message msg, long when) {
// 略
synchronized (this) {
// 略
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 {
// 略
}
if (needWake) {
nativeWake(mPtr); // 喚醒
}
}
return true;
}
Q:Looper.loop() 方法是一個(gè)死循環(huán)為什么不會阻塞APP
我認(rèn)為更好的回答:
這是一個(gè)假象始衅,舉個(gè)例子
public static void main(String[] args){
while(true){
// do work in while
}
doSomeThingOutWhile();
}
對于從整個(gè)main方法來看,while(true)
確實(shí)阻塞了 doSomeThingOutWhile()
這個(gè)方法的執(zhí)行缭保,對于這樣看汛闸,好像確實(shí)是卡住了,因?yàn)槲覀冊?doSomeThingOutWhile
方法中想要做的事沒法做了艺骂,但是如果我們把我們要做的事情通過隊(duì)列放到 while
里面去做诸老,那么是不是你就不會覺得卡了,你想要做的事情都完成了彻亲,雖然有個(gè)死循環(huán)但并不影響你想要做什么孕锄,而Android中 Looper.loop()
就是這樣的原理,因?yàn)樗凶屛覀儠X得卡住的都被放到 MessageQueue
里苞尝,然后通過Looper
取出并交給 Handler
執(zhí)行了畸肆。
PS:不僅僅是Android,幾乎所有和UI操作的都有一個(gè)類似Android Handler機(jī)制的事件循環(huán)處理機(jī)制
-----分割線-------
下面是原始回答宙址,會讓人覺得卡是因?yàn)樗姥h(huán)之后的代碼無法執(zhí)行轴脐,如果沒有理解到其實(shí)我們的代碼都是執(zhí)行在死循環(huán)里面的話,還是沒有辦法理解為什么不會卡抡砂。
如果說操作系統(tǒng)是由中斷驅(qū)動(dòng)的大咱,那么Android的應(yīng)用在宏觀上可以說是 Handler 機(jī)制驅(qū)動(dòng)的,所以主線程中的 Looper 不會一直阻塞的注益,原因如下(以下是我瞎JB猜的碴巾,歡迎補(bǔ)充、指正):
- 當(dāng)隊(duì)列中只有延遲消息的時(shí)候丑搔,阻塞的時(shí)間等于頭結(jié)點(diǎn)的 when 減去 當(dāng)前時(shí)間厦瓢,時(shí)間到了以后會自動(dòng)喚醒提揍。
- 在Android中 一個(gè)進(jìn)程中不會只有一個(gè)線程,由于 Handler 的機(jī)制煮仇,導(dǎo)致我們?nèi)绻僮?View 等都要通過 Handler 將事件發(fā)送到主線程中去劳跃,所以會喚醒阻塞。
- 傳感器的事件浙垫,如:觸摸事件刨仑、鍵盤輸入等。
- 繪制事件:我們知道要想顯示流暢那么屏幕必須保持 60fps的刷新率夹姥,那繪制事件在入隊(duì)列時(shí)也會喚醒杉武。
- 總是有
Message
源源不斷的被加入到MessageQueue
中去,事件是一環(huán)扣一環(huán)的佃声,舉個(gè)Fragment
的栗子:
getSupportFragmentManager()
.beginTransaction()
.replace(android.R.id.content,new MyFragment())
.commit();
這個(gè)時(shí)候并不是立馬把 MyFragment
顯示出來了艺智,而是經(jīng)過層層的調(diào)用來到了 FragmentManager#scheduleCommit()
方法,在這里又有入隊(duì)列操作圾亏,
// FragmentManager.java:2103
private void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit); // 這里有入隊(duì)列操作
}
}
}
提交后是不是緊接著又是一系列的生命周期的事件分發(fā)十拣?所以。志鹃。夭问。
你還有什么關(guān)于Handler的問題,評論告訴我吧
如果你還有什么在面試中遇到的和 Handler
相關(guān)的問題曹铃,該博客中沒有體現(xiàn)出來的趕緊評論告訴我吧缰趋,我會持續(xù)補(bǔ)充到這篇博客當(dāng)中。
我最近剛剛開通了微信公眾號(怪盜kidou)陕见,歡迎關(guān)注