內容摘要:Android Handler消息傳遞機制的學習總結、問題記錄 Handler消息傳遞機制的目的: 1.實現線程間通信(如:Android平臺只允許主線程(UI線程)修改Activity里的UI組件,而實際開發時會遇到新開的線程要改變界面組件屬性的情況,這時就要有一種辦法通知主線程更新UI ...
內容摘要:Android Handler消息傳遞機制的學習總結、問題記錄
Handler消息傳遞機制的目的:
1.實現線程間通信(如:Android平臺只允許主線程(UI線程)修改Activity里的UI組件,而實際開發時會遇到新開的線程要改變界面組件屬性的情況,這時就要有一種辦法通知主線程更新UI)。Handler消息傳遞機制可用於線程間傳遞消息。
2.實現消息的非同步處理。
機制的實現:(工作原理涉及Handler、Looper、Message(消息)、MessageQueue(消息隊列);代碼分消息接收方,發送方2處)
原理說明(本人理解有限,比較好的Handler說明看這篇):
Handler可發送Message到MessageQueue或處理從Looper收到的Message。
Message消息對象,在整個機制中傳遞。
MessageQueue是一個以先進先出方式管理Message的隊列。
Looper管理MessageQueue,它把從MessageQueue里取到的Message分發給相應的Handler。
原理圖:
註意:
1.Handler是建立在Looper上,實現Thread的消息系統處理模型,實現消息非同步處理的;
2.MessageQueue會在Looper(Looper()構造函數)初始化時創建關聯;
3.一個線程最多只能有一個Looper對象(Looper.prepare()方法創建Looper對象,規定了這個);
4.主線程(UI線程)系統已經幫初始化了一個Looper對象(簡單分析看這,主線程源碼詳細分析看這),因此程式直接創建Handler即可;程式員自己啟動的線程必須先創建Looper對象並啟動(調用Looper.loop()),然後才能向該線程的消息隊列發消息。
Looper源碼參考:
prepare()方法保證每個線程最多只有一個Looper對象,loop()方法使用一個死迴圈不斷取出MessageQueue中的消息,並把取出的消息分給對應的Handler處理。
1 //Looper初始化時創建並關聯MessageQueue 2 private Looper(boolean quitAllowed) { 3 mQueue = new MessageQueue(quitAllowed); 4 mThread = Thread.currentThread(); 5 }
1 //一個線程最多一個Looper,調用prepare()方法創建Looper對象 2 public static void prepare() { 3 prepare(true); 4 } 5 6 private static void prepare(boolean quitAllowed) { 7 if (sThreadLocal.get() != null) { 8 throw new RuntimeException("Only one Looper may be created per thread"); 9 } 10 sThreadLocal.set(new Looper(quitAllowed)); 11 }
1 /** 2 * Initialize the current thread as a looper, marking it as an 3 * application's main looper. The main looper for your application 4 * is created by the Android environment, so you should never need 5 * to call this function yourself. See also: {@link #prepare()} 6 */ 7 //主UI線程初始化Looper對象調用的方法 8 public static void prepareMainLooper() { 9 prepare(false); 10 synchronized (Looper.class) { 11 if (sMainLooper != null) { 12 throw new IllegalStateException("The main Looper has already been prepared."); 13 } 14 sMainLooper = myLooper(); 15 } 16 }
1 /** 2 * Run the message queue in this thread. Be sure to call 3 * {@link #quit()} to end the loop. 4 */ 5 public static void loop() { 6 final Looper me = myLooper(); 7 if (me == null) { 8 throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); 9 } 10 final MessageQueue queue = me.mQueue; 11 12 // Make sure the identity of this thread is that of the local process, 13 // and keep track of what that identity token actually is. 14 Binder.clearCallingIdentity(); 15 final long ident = Binder.clearCallingIdentity(); 16 17 //使用一個死迴圈不斷從MessageQueue取Message,併發給對應Handler 18 for (;;) { 19 Message msg = queue.next(); // might block 20 if (msg == null) { 21 // No message indicates that the message queue is quitting. 22 return; 23 } 24 25 // This must be in a local variable, in case a UI event sets the logger 26 final Printer logging = me.mLogging; 27 if (logging != null) { 28 logging.println(">>>>> Dispatching to " + msg.target + " " + 29 msg.callback + ": " + msg.what); 30 } 31 32 final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs; 33 34 final long traceTag = me.mTraceTag; 35 if (traceTag != 0 && Trace.isTagEnabled(traceTag)) { 36 Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); 37 } 38 final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); 39 final long end; 40 try { 41 msg.target.dispatchMessage(msg); 42 end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); 43 } finally { 44 if (traceTag != 0) { 45 Trace.traceEnd(traceTag); 46 } 47 } 48 if (slowDispatchThresholdMs > 0) { 49 final long time = end - start; 50 if (time > slowDispatchThresholdMs) { 51 Slog.w(TAG, "Dispatch took " + time + "ms on " 52 + Thread.currentThread().getName() + ", h=" + 53 msg.target + " cb=" + msg.callback + " msg=" + msg.what); 54 } 55 } 56 57 if (logging != null) { 58 logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); 59 } 60 61 // Make sure that during the course of dispatching the 62 // identity of the thread wasn't corrupted. 63 final long newIdent = Binder.clearCallingIdentity(); 64 if (ident != newIdent) { 65 Log.wtf(TAG, "Thread identity changed from 0x" 66 + Long.toHexString(ident) + " to 0x" 67 + Long.toHexString(newIdent) + " while dispatching to " 68 + msg.target.getClass().getName() + " " 69 + msg.callback + " what=" + msg.what); 70 } 71 72 msg.recycleUnchecked(); 73 } 74 }
原理總結到這,下麵看看在代碼中具體如何實現
代碼示例(只是一種實現,更多Handler用法總結看這):
1.消息接收方線程先調用Looper.prepare()創建Looper對象,然後創建Handler對象並定義處理消息的方法,接下來調用Looper.loop()啟動Looper。
1 class CallbackThread extends Thread { 2 public Handler mHandler; 3 4 public void run() { 5 Looper.prepare(); 6 mHandler = new Handler() { 7 @Override 8 public void handleMessage(Message msg) { 9 if (msg.what == 123) { 10 Toast.makeText(MainActivity.this, "get message!", Toast.LENGTH_SHORT).show(); 11 } 12 } 13 }; 14 Looper.loop(); 15 } 16 }
2.消息發送方線程通過調用Handler類相關方法向接收方線程的Handler對象發送消息,可用的方法有:
1 callbackThread = new CallbackThread(); 2 callbackThread.start(); 3 4 //發空消息 5 callbackThread.mHandler.sendEmptyMessage(123); 6 7 //創建消息發送 8 Message msg = new Message(); 9 msg.what = 123; 10 callbackThread.mHandler.sendMessage(msg);
以上是Handler學習總結,接下來是學習過程中遇到的問題記錄。
問題記錄
1.代碼示例中接收消息的線程是先調用Looper.prepare(),再創建Handler實現消息處理方法,最後再Looper.loop()。為什麼是prepare->Handler->loop這個順序?可不可以換?
答:順序不能換。首先prepare是肯定要在loop之前,因為prepare()方法源碼註釋中有這樣一句話(Be sure to call* {@link #loop()} after calling this method),那就按它的來(因為我沒看源碼......)。可還有兩種順序是吧,一個個試下。Handler->prepare->loop不行,現象是發出的消息沒被接收。prepare->loop->Handler也不可以,現象是導致APP退出。我(zhao)的(chao)理(wang)解(shang)是Handler要想正常工作首先要保證當前線程中有Looper對象(why?可能是能發消息首先要有消息隊列?),所以先要prepare創建Looper對象;然後Looper.loop()使用死迴圈取消息,且當沒有消息時會阻塞,這樣的話放在它之後的代碼——創建Handler的代碼不會執行,當調用該Handler對象的sendMessage()一類方法時便會產生NullPointerException,如下(AV畫質):
2.不是說只有UI線程能對UI組件操作,為什麼當上面截圖中代碼把創建Handler對象放在prepare和loop之間時,子線程使用Toast不報錯還能顯示?
答:首先子線程直接用Toast是不行的,不會彈出Toast只會報錯......之後我上網找啊找啊找到這篇,只是後面得出結論的時候說“Toast可能是屬於修改UI界面”???這幾個意思???於是我又找啊找啊,知乎找到這個問題,天啊!用了Toast這麼久難道它不是更新UI操作,可能正如知乎上大佬說的——“吐司操作的是window,不屬於checkThread拋主線程不能更新UI異常的管理範疇”?信息量太大,我能力有限還沒理解,先存疑記錄//Todo。總之,現在知道Toast要在子線程中使用可以藉助Looper。
3.Looper.loop()使用死迴圈取消息難道不會很耗資源嗎?
答:並不會,具體看這篇。簡而言之,死迴圈中調用queue.next()讀取下一條消息(在loop調用的線程中),如果讀取到了就msg.target.dispatchMessage(),否則queue.next()則會一直阻塞到超過超時時間。
4.主線程Looper也調用了loop(),會不會也阻塞?
答:也會有阻塞,但不會卡死,其實和問題3是一個道理,MessageQueue沒消息了都會阻塞進入休眠,之後會被句柄寫操作喚醒epoll.wait。參考:知乎問題,CSDN文章(雖然文章標題和結論矛盾)。
5.queue.next()的阻塞是怎麼實現的?
答:參考3,4中的鏈接。關鍵字:Linux pipe/epoll機制,loop()的queue.next()中的nativePollOnce()方法。
感想:第一篇博客花了我一晚上一早上加半個下午,媽呀!那些大佬都怎麼這麼高產的。問題其實還有更多的,但一部分忘了記錄下來,一部分太不成熟,再就是還存在沒發現的問題......越來越懵逼了,完全沒有豁然開朗的感覺???主要是知道的太少了,一次性見識到這麼多新的事物消化不來,學習筆記也很亂,畢竟第一次寫博客,慢慢學吧,Android之路長著呢,嘻嘻嘻!