標籤:
之前也是由於周末通宵看TI3比賽,一直沒找到時間寫部落格,導致已經有好久沒更新了。慚愧!後面還會恢複進度,盡量保證每周都寫吧。這裡也是先恭喜一下來自瑞典的Alliance戰隊奪得了TI3的冠軍,希望明年中國戰隊能夠虎起!
開始進入正題,我們都知道,Android UI是線程不安全的,如果在子線程中嘗試進行UI操作,程式就有可能會崩潰。相信大家在日常的工作當中都會經常遇到這個問題,解決的方案應該也是早已爛熟於心,即建立一個Message對象,然後藉助Handler發送出去,之後在Handler的handleMessage()方法中獲得剛才發送的Message對象,然後在這裡進行UI操作就不會再出現崩潰了。
這種處理方式被稱為非同步訊息處理線程,雖然我相信大家都會用,可是你知道它背後的原理是什麼樣的嗎?今天我們就來一起深入探究一下Handler和Message背後的秘密。
首先來看一下如何建立Handler對象。你可能會覺得挺納悶的,建立Handler有什麼好看的呢,直接new一下不就行了?確實,不過即使只是簡單new一下,還是有不少地方需要注意的,我們嘗試在程式中建立兩個Handler對象,一個在主線程中建立,一個在子線程中建立,代碼如下所示:
public class MainActivity extends Activity {private Handler handler1;private Handler handler2;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);handler1 = new Handler();new Thread(new Runnable() {@Overridepublic void run() {handler2 = new Handler();}}).start();}}
如果現在運行一下程式,你會發現,在子線程中建立的Handler是會導致程式崩潰的,提示的錯誤資訊為 Can‘t create handler inside thread that has not called Looper.prepare() 。說是不能在沒有調用Looper.prepare() 的線程中建立Handler,那我們嘗試在子線程中先調用一下Looper.prepare()呢,代碼如下所示:
new Thread(new Runnable() {@Overridepublic void run() {Looper.prepare();handler2 = new Handler();}}).start();
果然這樣就不會崩潰了,不過只滿足於此顯然是不夠的,我們來看下Handler的源碼,搞清楚為什麼不調用Looper.prepare()就不行呢。Handler的無參建構函式如下所示:
public Handler() { 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 = null;}
可以看到,在第10行調用了Looper.myLooper()方法擷取了一個Looper對象,如果Looper對象為空白,則會拋出一個運行時異常,提示的錯誤正是 Can‘t create handler inside thread that has not called Looper.prepare()!那什麼時候Looper對象才可能為空白呢?這就要看看Looper.myLooper()中的代碼了,如下所示:
public static final Looper myLooper() { return (Looper)sThreadLocal.get();}
這個方法非常簡單,就是從sThreadLocal對象中取出Looper。如果sThreadLocal中有Looper存在就返回Looper,如果沒有Looper存在自然就返回空了。因此你可以想象得到是在哪裡給sThreadLocal設定Looper了吧,當然是Looper.prepare()方法!我們來看下它的源碼:
public static final void prepare() { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper());}
可以看到,首先判斷sThreadLocal中是否已經存在Looper了,如果還沒有則建立一個新的Looper設定進去。這樣也就完全解釋了為什麼我們要先調用Looper.prepare()方法,才能建立Handler對象。同時也可以看出每個線程中最多隻會有一個Looper對象。
咦?不對呀!主線程中的Handler也沒有調用Looper.prepare()方法,為什麼就沒有崩潰呢?細心的朋友我相信都已經發現了這一點,這是由於在程式啟動的時候,系統已經幫我們自動調用了Looper.prepare()方法。查看ActivityThread中的main()方法,代碼如下所示:
public static void main(String[] args) { SamplingProfilerIntegration.start(); CloseGuard.setEnabled(false); Environment.initForCurrentUser(); EventLogger.setReporter(new EventLoggingReporter()); Process.setArgV0("<pre-initialized>"); Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread(); thread.attach(false); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } AsyncTask.init(); if (false) { Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread")); } Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");}
可以看到,在第7行調用了Looper.prepareMainLooper()方法,而這個方法又會再去調用Looper.prepare()方法,代碼如下所示:
public static final void prepareMainLooper() { prepare(); setMainLooper(myLooper()); if (Process.supportsProcesses()) { myLooper().mQueue.mQuitAllowed = false; }}
因此我們應用程式的主線程中會始終存在一個Looper對象,從而不需要再手動去調用Looper.prepare()方法了。
這樣基本就將Handler的建立過程完全搞明白了,總結一下就是在主線程中可以直接建立Handler對象,而在子線程中需要先調用Looper.prepare()才能建立Handler對象。
看完了如何建立Handler之後,接下來我們看一下如何發送訊息,這個流程相信大家也已經非常熟悉了,new出一個Message對象,然後可以使用setData()方法或arg參數等方式為訊息攜帶一些資料,再藉助Handler將訊息發送出去就可以了,範例程式碼如下:
new Thread(new Runnable() {@Overridepublic void run() {Message message = new Message();message.arg1 = 1;Bundle bundle = new Bundle();bundle.putString("data", "data");message.setData(bundle);handler.sendMessage(message);}}).start();
可是這裡Handler到底是把Message發送到哪裡去了呢?為什麼之後又可以在Handler的handleMessage()方法中重新得到這條Message呢?看來又需要通過閱讀源碼才能解除我們心中的疑惑了,Handler中提供了很多個發送訊息的方法,其中除了sendMessageAtFrontOfQueue()方法之外,其它的發送訊息方法最終都會輾轉調用到sendMessageAtTime()方法中,這個方法的源碼如下所示:
public boolean sendMessageAtTime(Message msg, long uptimeMillis){ boolean sent = false; MessageQueue queue = mQueue; if (queue != null) { msg.target = this; sent = queue.enqueueMessage(msg, uptimeMillis); } else { RuntimeException e = new RuntimeException( this + " sendMessageAtTime() called with no mQueue"); Log.w("Looper", e.getMessage(), e); } return sent;}
sendMessageAtTime()方法接收兩個參數,其中msg參數就是我們發送的Message對象,而uptimeMillis參數則表示發送訊息的時間,它的值等於自系統開機到目前時間的毫秒數再加上延遲時間,如果你調用的不是sendMessageDelayed()方法,延遲時間就為0,然後將這兩個參數都傳遞到MessageQueue的enqueueMessage()方法中。這個MessageQueue又是什麼東西呢?其實從名字上就可以看出了,它是一個訊息佇列,用於將所有收到的訊息以隊列的形式進行排列,並提供入隊和出隊的方法。這個類是在Looper的建構函式中建立的,因此一個Looper也就對應了一個MessageQueue。
那麼enqueueMessage()方法毫無疑問就是入隊的方法了,我們來看下這個方法的源碼:
final boolean enqueueMessage(Message msg, long when) { if (msg.when != 0) { throw new AndroidRuntimeException(msg + " This message is already in use."); } if (msg.target == null && !mQuitAllowed) { throw new RuntimeException("Main thread not allowed to quit"); } synchronized (this) { if (mQuiting) { RuntimeException e = new RuntimeException(msg.target + " sending message to a Handler on a dead thread"); Log.w("MessageQueue", e.getMessage(), e); return false; } else if (msg.target == null) { mQuiting = true; } msg.when = when; Message p = mMessages; if (p == null || when == 0 || when < p.when) { msg.next = p; mMessages = msg; this.notify(); } else { Message prev = null; while (p != null && p.when <= when) { prev = p; p = p.next; } msg.next = prev.next; prev.next = msg; this.notify(); } } return true;}
首先你要知道,MessageQueue並沒有使用一個集合把所有的訊息都儲存起來,它只使用了一個mMessages對象表示當前待處理的訊息。然後觀察上面的代碼的16~31行我們就可以看出,所謂的入隊其實就是將所有的訊息按時間來進行排序,這個時間當然就是我們剛才介紹的uptimeMillis參數。具體的操作方法就根據時間的順序調用msg.next,從而為每一個訊息指定它的下一個訊息是什麼。當然如果你是通過sendMessageAtFrontOfQueue()方法來發送訊息的,它也會調用enqueueMessage()來讓訊息入隊,只不過時間為0,這時會把mMessages賦值為新入隊的這條訊息,然後將這條訊息的next指定為剛才的mMessages,這樣也就完成了添加訊息到隊列頭部的操作。
現在入隊操作我們就已經看明白了,那出隊操作是在哪裡進行的呢?這個就需要看一看Looper.loop()方法的源碼了,如下所示:
public static final void loop() { Looper me = myLooper(); MessageQueue queue = me.mQueue; while (true) { Message msg = queue.next(); // might block if (msg != null) { if (msg.target == null) { return; } if (me.mLogging!= null) me.mLogging.println( ">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what ); msg.target.dispatchMessage(msg); if (me.mLogging!= null) me.mLogging.println( "<<<<< Finished to " + msg.target + " " + msg.callback); msg.recycle(); } }}
可以看到,這個方法從第4行開始,進入了一個死迴圈,然後不斷地調用的MessageQueue的next()方法,我想你已經猜到了,這個next()方法就是訊息佇列的出隊方法。不過由於這個方法的代碼稍微有點長,我就不貼出來了,它的簡單邏輯就是如果當前MessageQueue中存在mMessages(即待處理訊息),就將這個訊息出隊,然後讓下一條訊息成為mMessages,否則就進入一個阻塞狀態,一直等到有新的訊息入隊。繼續看loop()方法的第14行,每當有一個訊息出隊,就將它傳遞到msg.target的dispatchMessage()方法中,那這裡msg.target又是什麼呢?其實就是Handler啦,你觀察一下上面sendMessageAtTime()方法的第6行就可以看出來了。接下來當然就要看一看Handler中dispatchMessage()方法的源碼了,如下所示:
public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); }}
在第5行進行判斷,如果mCallback不為空白,則調用mCallback的handleMessage()方法,否則直接調用Handler的handleMessage()方法,並將訊息對象作為參數傳遞過去。這樣我相信大家就都明白了為什麼handleMessage()方法中可以擷取到之前發送的訊息了吧!
因此,一個最標準的非同步訊息處理線程的寫法應該是這樣:
class LooperThread extends Thread { public Handler mHandler; public void run() { Looper.prepare(); mHandler = new Handler() { public void handleMessage(Message msg) { // process incoming messages here } }; Looper.loop(); } }
當然,這段代碼是從Android官方文檔上複製的,不過大家現在再來看這段代碼,是不是理解的更加深刻了?
那麼我們還是要來繼續分析一下,為什麼使用非同步訊息處理的方式就可以對UI進行操作了呢?這是由於Handler總是依附於建立時所在的線程,比如我們的Handler是在主線程中建立的,而在子線程中又無法直接對UI進行操作,於是我們就通過一系列的發送訊息、入隊、出隊等環節,最後調用到了Handler的handleMessage()方法中,這時的handleMessage()方法已經是在主線程中啟動並執行,因而我們當然可以在這裡進行UI操作了。整個非同步訊息處理流程的如所示:
另外除了發送訊息之外,我們還有以下幾種方法可以在子線程中進行UI操作:
1. Handler的post()方法
2. View的post()方法
3. Activity的runOnUiThread()方法
我們先來看下Handler中的post()方法,代碼如下所示:
public final boolean post(Runnable r){ return sendMessageDelayed(getPostMessage(r), 0);}
原來這裡還是調用了sendMessageDelayed()方法去發送一條訊息啊,並且還使用了getPostMessage()方法將Runnable對象轉換成了一條訊息,我們來看下這個方法的源碼:
private final Message getPostMessage(Runnable r) { Message m = Message.obtain(); m.callback = r; return m;}
在這個方法中將訊息的callback欄位的值指定為傳入的Runnable對象。咦?這個callback欄位看起來有些眼熟啊,喔!在Handler的dispatchMessage()方法中原來有做一個檢查,如果Message的callback等於null才會去調用handleMessage()方法,否則就調用handleCallback()方法。那我們快來看下handleCallback()方法中的代碼吧:
private final void handleCallback(Message message) { message.callback.run();}
也太簡單了!竟然就是直接調用了一開始傳入的Runnable對象的run()方法。因此在子線程中通過Handler的post()方法進行UI操作就可以這麼寫:
public class MainActivity extends Activity {private Handler handler;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);handler = new Handler();new Thread(new Runnable() {@Overridepublic void run() {handler.post(new Runnable() {@Overridepublic void run() {// 在這裡進行UI操作}});}}).start();}}
雖然寫法上相差很多,但是原理是完全一樣的,我們在Runnable對象的run()方法裡更新UI,效果完全等同於在handleMessage()方法中更新UI。
然後再來看一下View中的post()方法,代碼如下所示:
public boolean post(Runnable action) { Handler handler; if (mAttachInfo != null) { handler = mAttachInfo.mHandler; } else { ViewRoot.getRunQueue().post(action); return true; } return handler.post(action);}
原來就是調用了Handler中的post()方法,我相信已經沒有什麼必要再做解釋了。
最後再來看一下Activity中的runOnUiThread()方法,代碼如下所示:
public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action); } else { action.run(); }}
如果當前的線程不等於UI線程(主線程),就去調用Handler的post()方法,否則就直接調用Runnable對象的run()方法。還有什麼會比這更清晰明了的嗎?
通過以上所有源碼的分析,我們已經發現了,不管是使用哪種方法在子線程中更新UI,其實背後的原理都是相同的,必須都要藉助非同步訊息處理的機制來實現,而我們又已經將這個機制的流程完全搞明白了,真是一件一本萬利的事情啊。
轉載《Android Handler、Message》