丨版權說明 : 《Android Handler機制簡單分析》於當前CSDN博客和乘月網屬同一原創,轉載請說明出處,謝謝。
本文一切從簡,將圍繞以下流程展開敘述:
what?
接觸Android的朋友都知道Handler機制用於多線程方面的通信,這好像是一句廢話。
why?
我們知道java幾個具有代表性的多線程通信方法,例如:
“wait”和”notify”通知機制
Java中每個類都是Oject的子類(萬物皆對象,滑稽~~),也就具備Oject的”wait()”和”notify()”方法特性。簡單舉例說明:兩個線程中,對於某類的對象a,在線程1中執行a.wait(),線程1則一直處於阻塞中,直到在線程2中執行a.notify(),線程1才被喚醒繼續執行。“synchronized”線程鎖機制
多個線程共享一個變量,通過上鎖( synchronized關鍵字 )限制線程們對該變量的訪問,誰拿到鎖,誰便可以對變量進行修改,待其他線程拿到鎖訪問該變量時,根據變量的變化作出相應的處理,以達到通信的目的。此處省略n個字…
嗯,上述方法都是利用線程阻塞
的方式進行通信。這若在Android中使用?你得先搞清楚3個問題:
Android中多線程通信是爲UI線程(主線程)+Worker線程(子線程)的交互服務的。
基於問題1,Android的UI線程不允許阻塞,否則會造成”ANR”( 想了解ANR? 傳送門)
基於問題2,爲避免”ANR”,Android中所有的耗時操作(如網絡請求,文件讀寫)須在子線程中完成,並通知進度或結果給主線程用於UI更新。
綜上:
既然java原生方法無法滿足Android程序設計方面的要求,那隻能另闢新徑了。還好google比較良心,自己挖“坑”自己補,於是設計了一系列UI線程與Worker線程通信的方法,如:
activity.runOnUiThread(Runable action)(Activity類下的切換回UI線程的方法)
view.post(Runable action),view.postDelayed(Runnable action, long delayMillis)(View類下的切換回UI線程的方法)
還有本文的主角Handler機制(異步消息處理機制)等等。
how?
先來一段Demo:
......
public class MainActivity extends AppCompatActivity {
private static final int MSG_DOWNLOAD_TASK = 1;
private TextView tv_progress;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_progress = findViewById(R.id.tv_progress);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_DOWNLOAD_TASK:
int progress = (int) msg.obj;
tv_progress.setText(progress + "");
break;
}
}
};
/**
* UI上的Button按鈕點擊事件
* 模擬執行下載任務
*
* @param view
*/
private void download(View view) {
new Thread(new Runnable() {
@Override
public void run() {
int progress = 0;
try {
while (progress >= 100) {
Message msg = Message.obtain();
msg.what = MSG_DOWNLOAD_TASK;
msg.obj = progress;
mHandler.sendMessage(msg);
/**
* 模擬下載進度回調中...
*/
Thread.sleep(1000);
progress++;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
上述demo便是Handler的簡單用法,希望大家能看懂。爲了簡練代碼,請忽略內存泄漏~~~
analyze
好了,知道怎麼用了,接下來就得知道爲什麼這樣寫可以切換到主線程,這就麻煩了,得看源碼!!!
怎麼看?直接通過demo看:
1.mHandler = new Handler() {... }
初始化Handler
- 來,我們來看看Handler構造方法在幹嘛:
>>> 下文所有源碼均源於Android8.0,爲了簡練,只保留核心代碼 <<<
public Handler() {
this(null, false);//走的是下面的雙參構造方法
}
public Handler(Callback callback, boolean async) {
......
mLooper = Looper.myLooper();//把當前線程中Looper對象引用交給Handler
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
//不能在此線程中創建handler,因爲還沒有調用過Looper.prepare()
}
mQueue = mLooper.mQueue;//從Looper對象取出MessageQueue對象給Handler
mCallback = callback;//null值
mAsynchronous = async;//false
}
上述代碼,我特意把拋異常的說明翻譯了一下,Excuse me?我並沒有執行啊,怎麼沒報異常?怎麼Looper.myLooper()有值的啊?
其實這並不矛盾,在同一個線程中可以創建一個或多個Handler對象,但前提必須是當前線程已創建(通過Looper.prepare()創建)並保存或已存在唯一的Looper對象(不理解沒關係,不瞭解Looper也沒有關係,下文會繼續說),Android所有線程之間的通信皆如此,主線程亦然。
Android中,app運行入口是在ActivityThread類裏的main函數開始的,沒錯,你沒看錯,就是java程序的入口main函數,android app也是java寫的,當然也是main入口的,那麼我們直接看核心源碼來解釋上面的疑問:
......
public final class ActivityThread {
......
public static void main(String[] args) {//app程序入口
......
//1.其實本質還是走Looper.prepare(),見下面Looper類相關代碼便知
Looper.prepareMainLooper();
......
if (sMainThreadHandler == null) {
//2.獲取的是Handle子類H對象引用,在H中添加了處理各種消息的業務(不理解沒關係,反正就是創建個Handler子類的對象)
sMainThreadHandler = thread.getHandler();
}
......
//3.輪詢消息
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
}
- Looper類下相關代碼:
......
public final class Looper {
......
public static void prepareMainLooper() {
//帶參的Looper.prepare(quitAllowed)方法
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
//已存在Looper對象了,不要再創建了
}
sMainLooper = myLooper();
}
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
//每個線程只能創建一個Looper對象,其實還是在說已存在Looper對象了,不要再創建了
}
//這裏創建了久違的Looper對象
sThreadLocal.set(new Looper(quitAllowed));
}
--------------順便看看Looper.prepare()在幹什麼--------------------
public static void prepare() {
/**
*本質是走上面的帶參的prepare(quitAllowed)方法
*不要太在意quitAllowed參數,反正是傳給Looper對象用的
*/
prepare(true);
}
}
--------------再來看Looper的初始化--------------------
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);//傳說中的MessageQueue(消息隊列)對象是在這裏創建的
mThread = Thread.currentThread();//獲取當前線程對象
}
小結:
1.由於是app程序入口,main函數一定執行在主線程(UI線程)上,並且程序一開始就爲主線程創建並保存好了Looper對象,以便爲Handler子類H提供服務,既然已存在,當然不需要自行“Looper.prepare()”了。
2.Android官方已經爲我們提供了Handler機制代碼模版↓↓↓
邏輯代碼寫法流程圖:
所以,可以這樣歸納:主線程與子線程間通信不需要寫Looper.prepare(…)和Looper.loop(),子線程與主線程以及子線程與子線程間的通信則需要。
3.MessageQueue(消息隊列)對象是在Looper初始化的時候被創建,且一個線程中僅能創建一個Looper對象,所以一個線程中MessageQueue與Looper對象是一對一的關係。
2. Message msg = Message.obtain();
子線程中發消息前創建Message對象
- 先簡單分析下
Message.obtain()
源碼:
/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*個人翻譯:從全局池返回一個新Message實例(可能是新創建的,也可能是從全局池中重新複用的)。
*允許我們在多數情況下避免分配(創建)過多的新對象
*/
public static Message obtain() {
synchronized (sPoolSync) {//同步鎖訪問機制
if (sPool != null) {//池不爲null,複用已存在的對象
Message m = sPool;//從池中取出Message對象(很明顯這個池也是Message類的對象)
sPool = m.next;
/**
*結合上面可以知道這個池其實是由多個Message對象組成的鏈表結構(不知道鏈表?找度娘...)
*每次複用的都是表頭的Message對象
*表頭被取走(複用)後,緊連着表頭的另一個Message對象成爲新的表頭,以此類推
*先不要想這個鏈表是怎麼添加Message對象的,也不要着急看Message類全部源碼,因爲不是本文重點
*/
m.next = null;//對即將複用的表頭(Message對象)進行脫鏈,從此自由啦!
m.flags = 0; //clear in-use flag (清除“在使用中的”的標記,恢復初始狀態以便複用)
sPoolSize--;//複用後,鏈表長度減1
return m;//返回表頭(複用表頭)
}
}
return new Message();//池爲null時直接創建新Message對象
}
- 下面介紹Message的flags屬性:
/**
* If set message is in use.
* This flag is set when the message is enqueued and remains set while it
* is delivered and afterwards when it is recycled. The flag is only cleared
* when a new message is created or obtained since that is the only time that
* applications are allowed to modify the contents of the message.
*
* It is an error to attempt to enqueue or recycle a message that is already in use.
*
*個人翻譯:這個值表示message在使用中(即:flags=FLAG_IN_USE=1,不是賦值號!!!)。當消息排隊時設
*置爲該標誌(設置爲"使用中"狀態),並且在傳送消息過程中保持該狀態,直到之後被回收。
*只有在新創建(“new Message()”)或獲取(“Message.obtain()”)一個消息時纔會清除該標誌,
*這是允許應用程序修改消息內容的唯一時間。
*
*當某消息處於使用中狀態時,嘗試去排隊或回收該消息是錯誤的。
*
*1 << 0還是等於1,不知道谷歌爲啥在很多源碼中都有這種騷操作,如果你知道請下方留言告知,萬分感謝!
*/
/*package*/ static final int FLAG_IN_USE = 1 << 0;
//使用狀態標識,默認爲0,即爲未使用
/*package*/ int flags;
再看
new Message()
構造方法源碼:/** * Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}). * 個人翻譯:構造器(但是推薦的方式是調用"Message.obtain()") */ public Message() { }
嗯哼,如此簡單明瞭的告訴你:其實我的構造方法沒啥騷操作,但希望你優先使用Message.obtain()方式獲取Message實例,避免鋪張浪費。
小結:
關於Message對象的獲取,優先考慮全局池(Message鏈表),有則取表頭並作脫鏈(next= null)和清除”in use”狀態(flags=0)的重置操作,無則“new”一個新對象,此時其flags默認值爲0,next爲null。這與上述翻譯的“清除‘in use’狀態的唯一時間”相對應。
下面是獲取Message對象流程圖
3.msg.what = MSG_DOWNLOAD_TASK......mHandler.sendMessage(msg);
發送消息
- 其實對於Message的what和obj用法大家應該很熟悉了,這裏就順便看一下源碼的解釋:
......
/**
* User-defined message code so that the recipient can identify
* what this message is about. Each {@link Handler} has its own name-space
* for message codes, so you do not need to worry about yours conflicting
* with other handlers.
*個人翻譯:用戶自定義的消息碼,便於接收者(Handler)識別是關於什麼的消息。
*每個Handler都有自己消息碼命名空間,所以你不用擔心與其他Handler衝突
*個人解釋:這是一個消息標識,隨便你怎麼定義這個消息碼的值,不用擔心因與其他Handler
*的Message消息碼相同而衝突,因爲Message對象由哪個handler對象發送,就由那個handler
*的handleMessage方法接收該消息(不明白?繼續看下文)
*/
public int what;
/**
* An arbitrary object to send to the recipient. When using
* {@link Messenger} to send the message across processes this can only
* be non-null if it contains a Parcelable of a framework class (not one
* implemented by the application). For other data transfer use
* {@link #setData}.
*
* Note that Parcelable objects here are not supported prior to
* the {@link android.os.Build.VERSION_CODES#FROYO} release.
*個人翻譯:一個任意對象發送給接收者。如果它是Parcelable實現類,使用Messenger
*跨進程(注意是"跨進程"哦)發送消息,只能是非空的。其它數據使用setData方法傳輸
*個人解釋:可以傳遞是任何Object類型對象,對於Messenger跨進程不是本文重點,請忽略。對於setData
*方法,方法全名爲setData(Bundle data),表示可以傳一個Bundle類型數據消息
*/
public Object obj;
......
- 接下來再回看Handler源碼:
......
/**
* Pushes a message onto the end of the message queue after all pending messages
* before the current time. It will be received in {@link #handleMessage},
* in the thread attached to this handler.
* 個人翻譯:推送一條消息到消息隊列,在所有此前處於等待中的消息之後排隊等待接收。這個消息
*將由綁定於當前handler對象的線程中被其handleMessage方法接收。
*個人解釋:當前時間發送的消息,會按先來後到的順序排隊等待被handler的handleMessage
*方法在handler被創建的線程中接收,好像還是很茫然哈,很正常,往後面看幾段你就明白了)
*
* @return Returns true if the message was successfully placed in to the
* message queue. Returns false on failure, usually because the
* looper processing the message queue is exiting.
個人翻譯:如果成功放入消息隊列返回true,如果失敗返回false,通常是這個消息隊列被輪詢的looper
*退出輪詢導致的
*/
public final boolean sendMessage(Message msg)
{
//走下面的方法。前面的方法介紹的很詳細,下面的方法就簡單介紹了
return sendMessageDelayed(msg,0);
}
/**
*顧名思義,就是延遲delayMillis毫秒後發送消息
*/
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
/**
*繼續走下面的方法,“SystemClock.uptimeMillis()”表示從系統開機到現在的毫秒數,
*類似於“System.currentTimeMillis()”,但不完全相同,這個不用糾結,反正就是表達“當前時間”。
*那麼“+ delayMillis”就是表示從當前時間開始向後延遲delayMillis毫秒
*/
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
/**
*同樣顧名思義,在指定的時間點開始發送消息,即指定從系統開機時間到uptimeMillis毫秒時開始發送消息
*貫穿上面的解釋很好理解,如果uptimeMillis=SystemClock.uptimeMillis()就是從此時開始,
*如果uptimeMillis=SystemClock.uptimeMillis()+delayMillis就是延後delayMillis毫秒開始。
*/
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
//通過上文可以知道這個消息隊列對象“mQueue”是在Handler初始化時由looper對象賦值給handler的
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
//走下面的方法
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
//當前Handler對象賦值給msg.target,小彩蛋:爲了之後告訴looper自己(msg)是被哪個handler對象發送的
msg.target = this;
/**
*是否爲異步傳輸,這個會打亂排隊順序(那排隊還有啥用?),所以不推薦使用
*並且通過上文可知Handler初始化時默認mAsynchronous爲false,所以這段代碼請忽略
*/
if (mAsynchronous) {
msg.setAsynchronous(true);
}
//哎,沒完沒了了,繼續走MessageQueue的enqueueMessage方法
return queue.enqueueMessage(msg, uptimeMillis);
}
......
- 看MessageQueue源碼:
......
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {//結合上文可知msg.target不爲null
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {//判斷Message是否爲"in use"狀態,結合上文可知爲0,即非使用狀態
throw new IllegalStateException(msg + " This message is already in use.");
}
synchronized (this) {
if (mQuitting) {//mQuitting默認爲false,只有調用quit()方法纔會爲true,先不考慮這裏
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();//沒錯,這裏就是Message回收到sPool的方法,不是本文重點,有興趣的可以看下
return false;//還記得sendMessage上的翻譯麼?"通常是這個消息...導致的"。說的就是這裏
}
msg.markInUse();//msg.flags設爲FLAG_IN_USE,即進入使用中狀態
msg.when = when;
/**
*這個mMessages便是真正的消息隊列實現者,其本質跟sPool一樣都是Message鏈表,
*且表頭也是優先級最高的,mMessages默認不會初始化,即mMessages==null
*/
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
//新的表頭,如果阻塞,喚醒事件隊列。
/**
*新的表頭?三個任意條件:mMessages==null,即隊列還不存在,當前來排隊的msg有幸成爲
*第一個排隊的,當然是表頭;when == 0,剛開機就發消息?優先級很高啊,當然放在表頭
*(雖然不太現實);when < p.when,比鏈表的表頭時間還小,肯定優先發送,當然要放在表
頭。
*/
msg.next = p;
mMessages = msg;//msg正式成爲表頭
......
} else {
......
/**
*重新組構鏈表,先按時間從小到大的順序排列,如果遇到時間點相同的msg則繼續按先來
*後到的順序排列
*小插曲:"先來後到"?是不是覺得很眼熟?上文"sendMessage"註釋表達的意思就是下方代
*碼的最終實現邏輯。
*/
Message prev;
for (;;) {//從頭到尾地拆鏈表,爲尋找msg合適的插入位置
prev = p;
p = p.next;//上下兩句代碼是在拆鏈
if (p == null || when < p.when) {
//p爲null表示爲表尾了,沒必要繼續拆了
//when < p.when表示當前msg已在排到最合適的位置了
break;
}
......
}
msg.next = p; // invariant: p == prev.next
prev.next = msg; //將msg插入鏈表中
}
......
}
return true;
}
......
小結:
因爲handler發送消息最終走的是sendMessageAtTime()
方法,所以enqueueMessage()
方法下的when
其實是指時間點
。在若干線程中,任意時間發送多個消息,如果最終調用enqueueMessage時傳入的when(即uptimeMillis
)值都相同,則它們被接收(處理)的時間點相同
。上文谷歌在sendMessage()
的註釋中提到”當前時間”是指調用sendMessage時,傳入的when,即”SystemClock.uptimeMillis() + delayMillis”與消息隊列中已有的某些msg
的when
值相同,需要按先來後到的順序排到這些msg的最後。
下面是消息隊列示意圖:
嗯哼,既然排好隊了,那是不是就等着Looper來輪詢了?Demo沒有給出輪詢代碼,因爲UI線程爲我們寫好了,你懂的。 接下來看analyze
收尾篇↓↓↓
4.Looper.loop();
輪詢消息
- 看 “Looper.loop()” 源碼:
......
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*個人翻譯:在此線程中運行消息隊列。 請務必調用 quit( )方法以結束循環。
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
//沒有Looper對象;"Looper.prepare()"未在此線程中調用。
//這個不用多說了吧?
}
final MessageQueue queue = me.mQueue;//獲取MessageQueue對象
......
for (;;) {//輪詢
//1.從消息隊列裏取消息
Message msg = queue.next(); // might block 可能會阻塞
if (msg == null) {
// No message indicates that the message queue is quitting.
//沒有消息表明消息隊列正在退出。
return;//退出輪詢
}
......
try {
msg.target.dispatchMessage(msg);//2.分發消息(交由與之關聯的handler接收處理)
......
} finally {
......
}
......
msg.recycleUnchecked();//3.消息處理結束,直接回收到全局池sPool
}
}
......
- 看看 “queue.next()” 獲取消息流程:
Message next() {
......
/**
* 線程阻塞的時間
*-1:一直阻塞,直到線程被喚醒爲止。如果期間有程序喚醒線程會立即向下執行。比如新消息進入隊列觸發喚醒
* 0:不阻塞
*>0: 最長阻塞nextPollTimeoutMillis毫秒。如果期間有程序喚醒線程會立即向下執行。比如新消息進入隊列觸發喚醒
*/
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {//需要阻塞
//在底層做好阻塞線程相關準備,主要是釋放已掛起的對象
Binder.flushPendingCommands();
}
//阻塞線程
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
//Try to retrieve the next message.Return if found.
//嘗試輪詢下一條消息。 如果找到則返回。
//當前時間
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
......//此處忽略了異步消息代碼
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
//下一條消息沒有準備好。 設置超時以在準備就緒時喚醒。
//消息接收處理的時間還沒到,計算休眠時間,以便下次來判斷此消息是否可以執行
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
//獲取一個msg
mBlocked = false;
if (prevMsg != null) {//不爲null屬於異步範圍,本文不考慮
prevMsg.next = msg.next;
} else {
mMessages = msg.next;//對msg(表頭)作脫鏈處理
}
msg.next = null;//msg徹底解放出來了
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();//標記爲使用中
return msg;//返回msg
}
} else {
// No more messages.
//沒有更多的消息了,讓線程一直阻塞,直到線程被喚醒(一般有新消息進入隊列會直接觸發喚醒)
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
// 處理完所有待處理消息後,立即退出輪詢。
if (mQuitting) {//消息隊列正在退出
dispose();//native底層註銷和釋放資源,完成退出
return null;
}
......//此處忽略了執行IdleHandler代碼
}
}
- 再看”msg.target.dispatchMessage(msg)”在幹什麼。高能預警↓↓↓
/**
* Handle system messages here.
*在這裏處理系統消息。
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
/**
* callback!=null表示通過postXXX()方法發送的消息,不需要走handleMessage方法
* 如:new Handler().postDelayed(Runnable r, long delayMillis);
*/
handleCallback(msg);
} else {
/**mCallback!=null表示創建Handler時直接傳入CallBack實現
*類,直接調用CallBack的handleMessage方法就好了
*如:new Handler(Callback callback);
*/
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
/**
*嗯哼,沒有傳callback的話,那就走Handler的handleMessage()方法渠道咯
*這也正是我們Demo所走的渠道,終於等到你,還好我沒放棄,額額額...
*/
handleMessage(msg);
}
}
小結:
“Looper.loop()”依賴兩個for循環來維持消息輪詢和分發,外環重複着三大任務:1.獲取消息(queue.next())。2.分發消息(msg.target.dispatchMessage(msg))。3.回收消息(msg.recycleUnchecked())。
外環由msg==null
條件成立而終止,爲了讓輪詢一直維持下去,queue.next()
作爲內環既要承擔這個任務,也要篩選msg提供給外環分發:1.有合適的msg則返回給外環。2.有消息但沒到分發時間點,則阻塞線程,最長阻塞nextPollTimeoutMillis毫秒喚醒,期間可能被一些因素喚醒,如有新消息進入隊列。3.無消息,即mMessages==null
,則一直阻塞線程,期間可能被一些因素喚醒,如有新消息進入隊列。4.如果消息隊列退出,即mQuitting==true
,則返回null,此時外環因msg==null
而終止。
可能還有人在疑問:哪裏能看得出handleMessage()
已經切換到目標線程了?這個問題我還真被人問過,這裏順便回答一下:因爲”handleMessage()”在”dispatchMessage()”下執行,而”dispatchMessage()”又在”loop()”下執行,”loop()”本身就運行在目標線程,這樣夠清晰了嗎?嗯?
至此,關於Handler機制的分析就告一段落了,寫作期間因爲各種原因中斷了很多次,也隔了很久,導致思路對接不通,不清晰,望請原諒,後期會不斷優化更新~~