Android版本: 基於API源碼26,Android版本8.0。
本片文章的目的在於全面的瞭解Handler。它是如何傳遞消息的?是如何阻塞和喚醒線程的(僅限於Java層面)?MessageQueue到底是怎麼存儲和取出Message?延遲消息是怎麼被髮送的?
一 、Handler定義:
Handler是一套消息傳遞的機制。設計用來讓工作線程跟主線程之間進行消息傳遞。同時也解決了多線程同時更新UI的問題。Android系統設定子線程是不能更新UI的,當子線程一定要做更新UI的操作時,可以使用Handler將消息從子線程帶到主線程,並能保證消息的同步性。
二 、基本用法:
- 在主線程中使用:
//實例化Handler對象。
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
//創建併發送消息
Message obtain = Message.obtain();
obtain.what = 100;
handler.sendMessage(obtain);
- 在子線程中使用,一旦使用該子線程是不會自動結束的,除非手動結束:
//爲子線程中創建Looper對象。
Looper.prepare();
//實例化Handler對象。
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
//創建併發送消息。
Message obtain = Message.obtain();
obtain.what = 100;
handler.sendMessage(obtain);
//開啓消息循環。
Looper.loop();
如果先調用Looper.prepare()、Looper.loop()再handler.sendMessage(obtain),那麼消息會成功的發送成功嗎?
三 、源碼解析:
Handle的消息傳遞過程中涉及到了幾個重要的類:Handler、Message、Looper、MessageQueue。
- Handler
官方的解釋:Handler允許您發送和處理Message
和MessageQueue
有關聯的可運行對象Runnable。每個Handler實例都與一個線程和該線程的消息隊列關聯,在創建的時候就會直接關聯起來。Handler將Message和Runnable傳遞到消息隊列中,並在它們從消息隊列中出來時執行它們。
Handler被用在兩個主要的地方:讓Message和Runnable定時執行;在非UI線程中將Message或者Runnable發送到主線程。
當應用程序創建進程時,其主線程專用於運行消息隊列,該隊列負責管理頂級應用程序對象(activities、broadcast receivers等)及其創建的任何窗口。
也就是Handler是用來專門發送和處理消息的。發送是隻發送到對應的消息隊列就好了。
- Message:
官方解釋:定義包含描述和可發送到{@link handler}的任意數據對象的消息。此對象包含兩個額外的int字段和一個extra對象字段,在許多情況下允許您不進行分配。
也就是Message是Handler傳遞的元數據,其內部可以包含很多信息:what、arg1、arg2,、obj等。Handler發送的也是Message對象,處理的也是Message對象。所以Message肯定是序列化的,繼承自Parcelable。內部維護了一個緩存池,避免創建多餘的消息對象,緩存池的設計是Android系統源碼一貫的作風,在ViewRootImpl處理點擊消息的時候,也有類似設計。該緩存池的大小爲50,超過該緩存就會直接創建Messsage對象。
- Looper:
官方解釋:Looper類用於爲線程運行消息循環。默認情況下,線程沒有與之關聯的消息循環;若要創建一個消息循環,請在要運行該循環的線程中調用 prepare()方法,然後調用loop()使其處理消息,直到循環停止。
也就是Handler只是將Message數據放到了MessageQunue中,Looper纔是從MessageQunue中取出消息,並將消息發送到正確的Handle中去。一個線程中存在一個Looper,Looper中維護着MessageQueue。Looper.prepare()方法是創建Looper跟MessageQueue,再發送消息之後調用Looper.loop()方法,開啓消息循環,也就是不停的從消息隊列中那出消息處理。
- MessageQueue:
該機制中核心的存在。主要是用來操作Message的,實際上內部並沒有一個集合或者隊列去存儲Message,而是由Message組成的單鏈表結構。在Message的內部有個next屬性,屬性聲明爲Message對象,也就是Message自身就可組成一個隊列。MessageQunue的目的就是增刪對比Message。比如:第一個Message對象來了(m1),那麼它的next屬性是空的,然後又來了一個Message(m2)。發現m1還沒執行完畢,那麼就將m2的對象賦值給m1的next屬性,當m1執行完畢之後,取出自己的next屬性,如果不爲null,那就接着執行next屬性表示的Message,也就是m2。
當了解完每個類的職責之後,發送的消息的流程就會清晰的很多。
一般的,一個Thread應該只有一個Looper和MessageQueue。可以有多個Handler對象。當你在主線程中創建Handler的時候,你是不需要調用Looper.prepare()的,因爲主線程已經創建過了。創建Looper的線程決定了消息最終是在那個線程中被處理的。你在子線程中使用Handler對象發送消息,如果你的Looper是主線程中創建的,那麼處理消息的還是在主線程中,你發送消息的線程環境是無關緊要的。
Handler創建源碼解析:
Handler類是個普通的Java類,可以被繼承。創建方式爲:
//一般通過new的方法去創建。
Handler handler = new Handler();
Handler有七個構造方法,三個是被隱藏掉的。被註解@hide註釋之後,通過api是訪問不到的:
//Handler.java
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);
}
/**
* @hide
*/
public Handler(boolean async) {
this(null, async);
}
/**
* @hide
*/
public Handler(Callback callback, boolean async) {
//。。。省略代碼
//獲取Looper對象。
mLooper = Looper.myLooper();
//。。。省略代碼
//獲取MessageQueue。
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
/**
* @hide
*/
public Handler(Looper looper, Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
可以看到構造參數中可以傳入的類型:Callback接口,Looper對象,布爾值async。首先Callback接口也是處理消息的:
//Handler.java
public interface Callback {
/**
* @param msg A {@link android.os.Message Message} object
* @return True if no further handling is desired
*/
public boolean handleMessage(Message msg);
}
Handler類內部也有handleMessage()方法,使用的時候完全可以重寫該方法。Handler又提供該接口的目的也就是你可以不用實現Handler類中的方法也能處理消息,這樣自由一點。
如果構造方法中你傳入了Looper對象,就不需要去創建Looper對象了。至於布爾值async則是決定當前發送的Message是否是異步的消息,異步消息可以不受同步屏障的影響。在ViewRootImpl接收到繪製信號的時候,就會在UI Handler中發送一個同步屏障,這將阻止當前Handler處理所有的同步消息,然後讓該此異步消息先執行。Handler的創建過程很簡單,接下來看Message的創建過程。
Message創建源碼解析:
Message類是final的不能被繼承。實現了Parcelable接口。Message並沒有特殊的構造方法,所以直接就可new出來:
Message message = new Message();
另外系統推薦的獲取Message的方法爲:
Message obtain = Message.obtain();
obtain()方法有很多的重載方法,根據參數的不同構造不同的Message對象。該過程都是在Message內部實現的,對於調用着來說是透明的,下面來分析下基礎的obtain()方法:
//Message.java。
public static Message obtain() {
synchronized (sPoolSync) {
//判斷緩存池是否爲null。
if (sPool != null) {
Message m = sPool;
//取出一個Message。
sPool = m.next;
m.next = null;
//默認的flag爲0。
m.flags = 0; // clear in-use flag
//大小減一。
sPoolSize--;
return m;
}
}
//否則就new一個
return new Message();
}
sPool是一個Message對象,相當於緩存隊列的頭指針。Mesage有個next屬性,該屬性還是Message對象。當調用該方法的時候,就從sPool中取出下一個Message也就是m.next,然後將m.next賦值給sPool,也就是重置隊列頭,然後返回之前的隊列頭m。這種是典型的單向鏈表結構,Android系統的源碼中很多地方都用到。然後在Message的reaycle()方法中,將使用過的Message存放到sPool代表的隊列中。
//Message.java。
public void recycle() {
if (isInUse()) {
return;
}
recycleUnchecked();
}
void recycleUnchecked() {
//。。。省略代碼
synchronized (sPoolSync) {
//判斷緩存的數量是否小於規定值的
if (sPoolSize < MAX_POOL_SIZE) {
//將隊列頭指針指向下一個元素
next = sPool;
//隊列頭重置,這樣新的消息就變成了隊列的第一個元素,之前sPool表示的消息就變成該消息的next。
sPool = this;
sPoolSize++;
}
}
}
Message的創建很簡單,需要注意的就是這個緩存池的原理。
消息發送流程源碼解析:
Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
return false;
}
});
Message obtain = Message.obtain();
obtain.what = 100;
handler.sendMessage(obtain);
上述代碼即可發送一個消息數據。不管是sendMessage()還是sendMessageDelayed()或者sendEmptyMessage()方法,最終都是調用的sendMessageAtTime()方法,sendEmpty***系列是Handler自己創建了一個空的Message,只是在使用着看起來是發送了一個空的消息一樣,這裏不再分析:
//Handler.java。
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
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);
}
該方法中只是判斷了MessageQueue是否爲null。
//Handler.java。
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
//該消息的目標Handler爲當前發送消息的Handler。
msg.target = this;
//如果是異步消息的話就設置爲異步標記。
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
對Message對象做了一些標記之後就調用了MessageQueue的enqueueMessage()方法,這兩個標記都是很重要的,一個是設置目標Handler,這樣消息就不會讓錯誤的Handler處理。另外一個是表明消息是否是異步的,一般使用的時候都是同步消息,UI線程畢竟是線程不安全的:
//Message.java。
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}
接着看queue.enqueueMessage()方法:
//MessageQueue.java。
boolean enqueueMessage(Message msg, long when) {
//如果消息沒有目標Handler就拋出異常。
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) {
//當前的消息隊列已經結束了。mQuitting = true。
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;
//mMessages表示當前準備處理消息。
Message p = mMessages;
boolean needWake;
//1.當前要處理的消息爲null。2.需要馬上發送,延遲時間爲0。3.如果當前要處理的消息的延遲比正在處理的消息時間段。
//上面三個條件滿足一個就執行。如果是第一條消息if肯定會執行,如果來了第二條Message,但是第一條Message還沒執行完,也就是mMessages不爲null,消息的when還等於0,那麼也要直接執行,也就是添加到隊列的頭部。所以markInUse()方法還是很有必要的。
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 {
//當下一條消息來的時候發現前面還有消息沒執行完,並且當前的消息也不是立馬就執行的,或者等待的時間比正在執行的消息要長,那麼就執行到這邊。
//p.target == null表示當前的消息是一個同步屏障。
//needWake = true,說明當前的前一條消息是個同步屏障,並且當前的這條消息是個異步消息。
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;
}
//插入隊列中間。通常我們不必喚醒事件隊列,除非隊列的開頭有一個屏障,並且消息是隊列中最早的異步消息。
//判斷是否喚醒線程。
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
上面的入對列操作很複雜:
- 一條新的消息到了,調用消息的markInUse方法。
- 判斷是否要立馬執行該消息。條件有三個:當前要處理的消息爲null;延遲時間爲0;如果當前要處理的消息的延遲時間比正在處理的消息時間短。
- 滿足步驟2的三個條件,就將當前的消息放到消息對列的頭部位置。needWake變量賦值。
- 當一條新的消息到來的時候,不滿足步驟2,也就是當前有正在執行的消息,並且該消息需要延遲發送,延遲的時間還比較長。那麼就會執行else方法。
- 走到else方法中,要先判斷當前正在執行的Message是否是一個同步屏障,也就是 p.target == null,然後判斷當前的消息是否是一個異步消息,因爲異步消息的話,是需要立馬執行的。然後有個For循環,判斷當前的隊列中是否有個異步消息還在處理中,有的話也就是 if (needWake && p.isAsynchronous())會執行,那麼此條消息就不能立馬執行。否則就要加入到隊列中等待了。
- 判斷needWake,是否喚醒線程。
經過上面的步驟,Handler將發送的Message放入到了消息隊列中。Handler類的職責已經結束了。下面來看Looper到消息隊列中取數據。
Looper源碼解析:
Looper類的註釋中說明了,在使用的時候要先調用Looper.prepare()方法,最後調用Looper.loop()方法。下面先看下prepare()方法:
//Looper.java。
//該方法是可以在任何線程中調用。
public static void prepare() {
prepare(true);
}
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));
}
//該方法在ActivityThread創建的時候會被調用。
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
prepare()系列方法就是創建一個Looper對象。採用prepareMainLooper()方法創建的Looper不會終止消息循環,其他的都可以被終止掉,也就是quitAllowed = true/fasle的問題。Looper創建結束之後看下loop()方法:
//Looper.java。
public static void loop() {
final Looper me = myLooper();
//。。。省略日誌代碼
//Handler跟Looper用的是同一個隊列。
final MessageQueue queue = me.mQueue;
//確保此線程的標識是本地進程的標識,並跟蹤該標識令牌的實際內容。
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
//從消息隊列中取出下一條消息。
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
//。。。省略日誌代碼
try {
//msg.target就是Handler,然後分發消息。
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
//。。。省略日誌代碼
}
//。。。省略日誌代碼
//消息處理結束之後回收消息。
msg.recycleUnchecked();
}
}
loop()方法要做的就是不停的從MessageQueue中獲取消息,所以就使用了For死循環。拿到消息之後,開始分發消息,最後將消息回收。重點在於Message的next()和Handler的dispatchMessage()以及最後Message的recycleUnchecked()方法了。先看next()方法:
//MessageQueue.java
Message next() {
//如果消息循環已經退出並被釋放,則返回此處。如果應用程序在退出後嘗試重新啓動不受支持的循環程序,則可能發生這種情況。
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
//空閒時待處理的IdleHandler的數量。默認爲-1。
int pendingIdleHandlerCount = -1; // -1 only during first iteration
//下次輪詢超時毫秒
int nextPollTimeoutMillis = 0;
//開啓死循環。要麼阻塞,要麼獲取到Message。
for (;;) {
//將當前線程中掛起的任何綁定器命令刷新到內核驅動程序。在執行可能會阻塞很長時間的操作之前調用,以確保已釋放任何掛起的對象引用,以防止進程佔用對象的時間超過所需時間,這將非常有用。
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//該方法可能會被阻塞,nextPollTimeoutMillis = -1的時候,將無限期的阻塞。也就是說當該方法有返回值的時候,那就代表該過程不會被阻塞,也就是會執行下面的代碼邏輯,也就是將會至少取出一個消息。
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// 獲取系統時間。
final long now = SystemClock.uptimeMillis();
//該Message是表示將被執行的消息的前一個消息,有可能是從消息隊列的中間取出一個消息執行,這樣需要將被取出消息的next拼接到被取出消息的上一個消息的next上,這樣隊列不會被斷開。
Message prevMsg = null;
//隊列的頭消息。每次取出一個消息的時候都會將mMessages指向新的消息。
Message msg = mMessages;
if (msg != null && msg.target == null) {
//執行到這裏說明當前的消息是一個同步屏障,也就是將阻止所有的同步消息。
//找出隊列中的第一個異步消息,沒有異步消息msg就是null。
do {
//找出消息的上一個消息。
prevMsg = msg;
//當前找出的消息。
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//這裏並不是else語句,所以就是不是同步屏障就直接執行到這裏。
if (msg != null) {
if (now < msg.when) {
// 下一條消息尚未準備好。設置一個超時,以便在它準備好時喚醒。也就是nextPollTimeoutMillis參數其實就是上面nativePollOnce()本地方法的參數,也就是阻塞的時長。
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//走到這裏說明找到了符合要求的Message,準備返回了。
mBlocked = false;
if (prevMsg != null) {
//prevMsg不爲null表示當前的msg是一個異步消息,那麼其值表示msg消息的上一個消息。也就是msg是從隊列中間取出來的,那麼就需要把隊列拼接完整了。
prevMsg.next = msg.next;
} else {
//走到這裏說明msg是一個同步消息,也是從隊列的頭部獲取到的,那麼就重置mMessages爲msg的下一個消息。
mMessages = msg.next;
}
//清空該消息的next屬性,這樣該消息就是一個獨立的消息。在上一步中該消息的next屬性已經賦給了隊列中的其它Message。
msg.next = null;
//標記正在使用
msg.markInUse();
return msg;
}
} else {
// 沒有消息就爲-1.-1就表示該線程將無限期的阻塞。
nextPollTimeoutMillis = -1;
}
//處理完所有掛起的消息後,立即處理退出消息。
if (mQuitting) {
dispose();
return null;
}
//。。。省略了空閒時處理IdleHandler的邏輯。IdleHandler對象,IdleHandler是一個接口,也就是繼承了該接口的事物將會在MessageQueue空閒的時候去處理。GC機制就是採用的這個東西觸發的。
if (pendingIdleHandlerCount <= 0) {
//沒有東西可以執行了,就開始等待工作。
mBlocked = true;
//continue繼續執行for循環,然後在執行nativePollOnce()方法,線程阻塞。
continue;
}
}
}
在Looper的loop()方法中第一個重要的就是MessageQueue的next()方法。該方法有以下幾點:
- 先開啓一個For死循環,做的跟Looper的loop()方法一樣。之後先阻塞線程,通過調用nativePollOnce()方法。當線程被喚起,也就是nativePollOnce()方法有返回值了,就執行後面的邏輯。
- 先獲取當前的時間,然後跟消息的延遲時間做對比。mMessages變量表示的就是消息隊列中的頭佈局,在MessageQueue的equeueMessage()方法中先被賦值,先是說第一此賦值的,接着在next()方法中還會被賦值。
- 判斷Message是不是一個同步屏障,稍後解釋。也就是
msg.target == null
,是的話就要拿出隊列中第一個異步消息,也就是Message的isAsynchronous()方法返回true。如果不是同步屏障,就繼續往下走。 - 這裏再次都Message判空,是因爲上一步,如果當前的消息是同步屏障的話,又找不到異步消息,那就可能返回null。
- 消息不爲null的時候,就判斷消息的延遲時間是否已經到了。如果還沒到的話,就計算出剩餘的事件,然後傳值給nextPollTimeoutMillis變量,第1步的時候nativePollOnce()方法會用到該值,判斷倒計時多久之後喚醒線程。所以該倒計時時間確定之後,MessageQueue的next()方法中的for循環再次執行到nativePollOnce()方法的時候,就會執行該倒計時。
- 如果需要立馬執行的話,mBlocked先變爲false,也就是在MessageQueue的queueMessage()方法此時添加Message的時候就不用喚醒線程了。之後判斷prevMsg是否爲null,prevMsg表示了當前要執行的消息msg的前一個消息,不爲null,表示msg是從隊列中間位置取出來的執行的,那麼這一步就得把隊列繼續鏈接起來。如果prevMsg爲null,也就是說msg是從隊列中的第一個位置取出的,只需要將mMessages變量重新指向msg的下一個消息就好了。
- 將要執行的消息msg的next屬性清空,然後返回。
- 如果出現了同步屏障,並且下一個異步消息也沒找到,那就是msg就是null 的,nextPollTimeoutMillis = -1,for循環的下次將會被阻塞住,這是個next()方法中的for死循環。
- 判斷是否要退出。主線程是不允許被退出的。
- 處理那些空閒的時候才處理的IdleHandler事件,然後結束之後,mBlocked被置爲true。也就是說當MessageQueue中沒有任何事情可做的時候,mBlocked纔是true。
上述就是MessageQueue的next()方法。經過該方法後繼續看Looper的loop()方法:
//Looper.java。
public static void loop() {
//。。。省略代碼
for (;;) {
//。。。省略代碼
try {
//msg.target就是Handler,然後分發消息。
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
//。。。省略日誌代碼
}
//。。。省略日誌代碼
//消息處理結束之後回收消息。
msg.recycleUnchecked();
}
}
這裏獲取到Message的target屬性,然後調用了dispatchMessage()方法。target屬性是在Handler的enqueueMessage()方法中賦值的:
//Handler.java
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
也就是當前Handler對象。之後就是Handle的分發方法:
//Handler.java
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
代碼一目瞭然。如果msg.callback不爲null,說明當前的消息是Runnable對象。不是的話,判斷mCallback是否爲null,也就是Handler的構造方法中的參數Callback,不爲null就調用Callback的handleMessage()方法。再接着就是調用自身的handleMessage()方法。到這Handle的源碼分析就結束了!重點難點全在MessageQueue類中。
好了,文章到這裏就結束瞭如果你覺得文章還算有用的話,不妨把它們推薦給你的朋友。
漫漫開發之路,我們只是其中的一小部分……只有不斷的學習、進階,纔是我們的出路!纔跟得上時代的進步!
今年年初我花一個月的時間收錄整理了一套知識體系,如果有想法深入的系統化的去學習的,可以私信我【學習】,我會把我收錄整理的資料都送給大家,幫助大家更快的進階。