標籤:
本文包括與Handler有關的以下問題:
(1)Handler的作用
(2)為什麼Android中要設計為只能在UI線程中去更新UI呢?
(3)Handler的兩個異常
(4)Handler、Looper MessageQueue之間的關係(源碼角度)
(5)跟線程相關的Handler,即HandlerThread(源碼角度分析)
(6)主線程往子線程發訊息
一、Handler的作用:
(1)在非UI線程中完成耗時操作,在UI線程中去更新UI。
(2)可以在主線程中發送延時訊息。
二、為什麼Android中要設計為只能在UI線程中去更新UI呢?
(1)解決多線程並發問題(根本原因)
(2)提高介面更新的效能問題
(3)架構設計的簡單
你可能會說,既然是擔心多線程並發問題,那我在子線程中加鎖進行更新UI行不行呢?你這樣想的話,會容易造成UI卡頓的。
注1:大部分面試者很難去說出一個令面試官滿意的答案。
注2:關於多線程,這裡舉一個例子,比如說銀行取款的問題。正常情況下,銀行卡餘額不能少於取款金額,如果多線程進行取款的話,就會造成線程不安全。
注3:Android中之所以說架構簡單,是因為幫我們封裝了很多更新UI的操作。
三、Handler的兩個異常:
在使用Handler時,經常會出現以下兩個異常:
(1)CalledFromWrongThreadException:這種異常是因為嘗試在子線程中去更新UI,進而產生異常。
(2)Can‘t create handle inside thread that ha not called Looper.prepared:是因為我們在子線程中去建立Handler,而產生的異常。
我們接下來通過代碼來把這兩個異常示範一下。
1、子線程中更新UI的異常:
(1)activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world"/> <Button android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="按鈕"/></RelativeLayout>
上方代碼中,一個文本,一個按鈕,代碼比較簡單。
(2)MainActivity.java:
1 import android.app.Activity; 2 import android.os.Bundle; 3 import android.view.View; 4 import android.widget.Button; 5 import android.widget.TextView; 6 7 public class MainActivity extends Activity { 8 9 private TextView tv;10 private Button btn;11 12 @Override13 protected void onCreate(Bundle savedInstanceState) {14 super.onCreate(savedInstanceState);15 setContentView(R.layout.activity_main);16 tv = (TextView) findViewById(R.id.tv);17 btn = (Button) findViewById(R.id.btn);18 19 //點擊按鈕後,嘗試在子線程中更新UI20 btn.setOnClickListener(new View.OnClickListener() {21 @Override22 public void onClick(View v) {23 24 new Thread(new Runnable() {25 @Override26 public void run() {27 tv.setText("smyhvae"); //子線程中更新UI28 }29 }).start();30 31 }32 });33 }34 }
上方代碼中,核心代碼是第27行:點擊按鈕後,在子線程中更新UI。
運行程式後,點擊按鈕,效果如下:
此時,我們來看一下背景log日誌:
中報的錯誤記錄檔就是因為我們在子線程中去更新UI。
解決方案:
在子線程中建立Message訊息,通過Handler發給主線程,之後在Handler的handleMessage方法中獲得Message訊息,進而處理更新UI介面。代碼如下:
1 import android.app.Activity; 2 import android.os.Bundle; 3 import android.os.Handler; 4 import android.os.Message; 5 import android.view.View; 6 import android.view.View.OnClickListener; 7 import android.widget.Button; 8 import android.widget.TextView; 9 10 public class MainActivity extends Activity implements OnClickListener {11 public static final int UPDATE_TEXT = 1;12 private TextView text;13 private Button changeText;14 15 //程式一載入,直接在主線程中建立Handler16 private Handler handler = new Handler() {17 public void handleMessage(Message msg) {18 switch (msg.what) {19 case UPDATE_TEXT:20 text.setText("Nice to meet you");21 break;22 default:23 break;24 }25 }26 };27 28 @Override29 protected void onCreate(Bundle savedInstanceState) {30 super.onCreate(savedInstanceState);31 setContentView(R.layout.activity_main);32 text = (TextView) findViewById(R.id.text);33 changeText = (Button) findViewById(R.id.change_text);34 changeText.setOnClickListener(this);35 }36 37 @Override38 public void onClick(View v) {39 switch (v.getId()) {40 case R.id.change_text:41 new Thread(new Runnable() {42 @Override43 public void run() {44 Message message = new Message();45 message.what = UPDATE_TEXT;46 handler.sendMessage(message);47 }48 }).start();49 break;50 default:51 break;52 }53 }54 }
上方第44行代碼也可以換成:
Message message = handler.obtainMessage();
2、在子線程中建立Handler的異常:
MainActivity.java:
1 import android.app.Activity; 2 import android.os.Bundle; 3 import android.os.Handler; 4 import android.widget.TextView; 5 6 public class MainActivity extends Activity { 7 8 private TextView tv; 9 10 @Override11 protected void onCreate(Bundle savedInstanceState) {12 super.onCreate(savedInstanceState);13 setContentView(R.layout.activity_main);14 tv = (TextView) findViewById(R.id.tv);15 16 //嘗試在子線程中去建立Handler17 new Thread(new Runnable() {18 @Override19 public void run() {20 new Handler();21 }22 }).start();23 }24 }
運行程式後, 報錯如下:
四、Handler、Looper MessageQueue之間的關係:(源碼角度)
如果要問到Handler,這個問題基本是面試必問的。
原理分析:
Handler是Android類庫提供的用於發送、處理訊息或Runnable對象的處理類,它結合Message、MessageQueue和Looper類以及當前線程實現了一個訊息迴圈機制,用於實現任務的非同步載入和處理。整個非同步訊息處理流程的如所示:
根據上面的圖片,我們現在來解析一下非同步訊息處理機制:
- Message:訊息體,用於裝載需要發送的對象。
- Handler:它直接繼承自Object。作用是:在子線程中發送Message或者Runnable對象到MessageQueue中;在UI線程中接收、處理從MessageQueue分發出來的Message或者Runnable對象。發送訊息一般使用Handler的sendMessage()方法,而發出去的訊息經過處理後最終會傳遞到Handler的handlerMessage()方法中。
- MessageQueue:用於存放Message或Runnable對象的訊息佇列。它由對應的Looper對象建立,並由Looper對象管理。每個線程中都只會有一個MessageQueue對象。
- Looper:是每個線程中的MessageQueue的管家,負責接收和分發Message或Runnable的工作。調用Looper.loop()方法,就是一個死迴圈,不斷地從MessageQueue中取訊息:如果有訊息,就取出,並調用Handler的handlerMessage()方法;如果沒有訊息阻塞。
現在可以做出如下總結:
(1)Handler負責發送訊息,Looper負責接收Handler發送的訊息放到MessageQueue,Looper又將訊息回傳給Handler自己。
(2)一個Handler對應一個Looper對象,一個Looper對應一個MessageQueue對象(Looper內部包含一個MessageQueue),一個Handler可以產生多個Message。
(3)Handler就是公開給外部線程的介面,用於線程間的通訊。Looper是由系統支援的用於建立和管理MessageQueue的依附於一個線程的迴圈處理對象,而Handler是用於操作線程內部的訊息佇列的,所以 Handler也必須依附一個線程,而且只能是一個線程。
(4)由於Handler是在主線程中建立的,所以此時handleMessage()方法中的代碼也會在主線程中運行,於是我們在這裡就可以安心地進行UI操作了
生活中的例子:
中,可以這麼理解:開會時,我(Handler)想中途離開去做別的事情,通過sendMessage發訊息給領導,領導思考了一會兒,同意之後,通過Looper.looep()方法將訊息回傳給我,說我可以離開,然後我就調用handleMessage方法去做別的事情去了。
註:面試的時候,如果只是從字面的角度來解釋Handler、Looper MessageQueue之間的關係,並不能夠真正打動面試官,倒不如再舉一個生動的例子,讓面試官覺得你是懂物件導向的思維的。
五、跟線程相關的Handler,即HandlerThread(源碼角度分析)
這個問題可以看一下這篇部落格:
http://blog.csdn.net/lmj623565791/article/details/47079737
六、主線程往子線程發訊息:(考察你是否真的理解Handler機制)
我們在平時開發的過程中,經常是子線程往主線程中發訊息,讓主線程更新UI。但是根據具體的項目需求,也可能會要求讓你在主線程中往子線程中發訊息。
1 import android.app.Activity; 2 import android.os.Bundle; 3 import android.os.Handler; 4 import android.os.Looper; 5 import android.os.Message; 6 import android.util.Log; 7 import android.view.View; 8 import android.view.View.OnClickListener; 9 import android.widget.Button;10 import android.widget.TextView;11 12 public class MainActivity extends Activity implements OnClickListener {13 public static final int UPDATE_TEXT = 1;14 private TextView tv;15 private Button btn;16 private Handler handler;17 18 @Override19 protected void onCreate(Bundle savedInstanceState) {20 super.onCreate(savedInstanceState);21 setContentView(R.layout.activity_main);22 tv = (TextView) findViewById(R.id.tv);23 btn = (Button) findViewById(R.id.btn);24 btn.setOnClickListener(this);25 //疑問:為什麼這段代碼如果寫在onClick方法裡面會報null 指標?26 new Thread(new Runnable() {27 @Override28 public void run() {29 //1、準備Looper對象30 Looper.prepare();31 //2、在子線程中建立Handler32 handler = new Handler() {33 @Override34 public void handleMessage(Message msg) {35 super.handleMessage(msg);36 Log.i("handleMessage:", Thread.currentThread().getName());37 Log.i("後台輸出", "收到了訊息對象");38 }39 };40 //3、調用Looper的loop()方法,取出訊息對象41 Looper.loop();42 }43 }).start();44 45 }46 @Override47 public void onClick(View v) {48 Log.i("onClick:", Thread.currentThread().getName());49 switch (v.getId()) {50 case R.id.btn:51 Message msg = handler.obtainMessage();52 handler.sendMessage(msg);53 break;54 55 default:56 break;57 }58 }59 }
上方的第29行至41行代碼:這是MainThread中發送訊息,在子線程中接收訊息的固定寫法。上面的三個步驟再重複一下:
- 準備Looper對象
- 在WorkerThread當中產生一個Handler對象
- 調用Looper的loop()方法之後,Looper對象將不斷地從訊息佇列當中取出對象,然後調用handler的handleMessage()方法,處理該訊息對象;如果訊息佇列中沒有對象,則該線程阻塞
注意,此時handleMessage()方法是在子線程中啟動並執行。
後台運行效果:
小小地總結一下:
首先執行Looper的prepare()方法,這個方法有兩個作用:一是產生Looper對象,而是把Looper對象和當前線程對象形成索引值對(線程為鍵),存放在ThreadLocal當中,然後產生handler對象,調用Looper的myLooper()方法,得到與Handler所對應的Looper對象,這樣的話,handler、looper 、訊息佇列就形成了一一對應的關係,然後執行上面的第三個步驟,即Looper在訊息佇列當中迴圈的取資料。
另外,在本文最開頭的第一段中,我們在主線程中建立Handler也沒有調用Looper.prepare()方法,為什麼就沒有崩潰呢?,這是由於在程式啟動的時候,系統已經幫我們自動調用了Looper.prepare()方法。查看ActivityThread中的main()方法,代碼如下所示:
1 public static void main(String[] args) { 2 SamplingProfilerIntegration.start(); 3 CloseGuard.setEnabled(false); 4 Environment.initForCurrentUser(); 5 EventLogger.setReporter(new EventLoggingReporter()); 6 Process.setArgV0("<pre-initialized>"); 7 Looper.prepareMainLooper(); 8 ActivityThread thread = new ActivityThread(); 9 thread.attach(false); 10 if (sMainThreadHandler == null) { 11 sMainThreadHandler = thread.getHandler(); 12 } 13 AsyncTask.init(); 14 if (false) { 15 Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread")); 16 } 17 Looper.loop(); 18 throw new RuntimeException("Main thread loop unexpectedly exited"); 19 }
上方代碼中,可以看到,在第7行調用了Looper.prepareMainLooper()方法,而這個方法又會再去調用Looper.prepare()方法,代碼如下所示:
1 public static final void prepareMainLooper() { 2 prepare(); 3 setMainLooper(myLooper()); 4 if (Process.supportsProcesses()) { 5 myLooper().mQueue.mQuitAllowed = false; 6 } 7 }
總結:這樣基本就將Handler的建立過程完全搞明白了,總結一下就是在主線程中可以直接建立Handler對象,而在子線程中需要先調用Looper.prepare()才能建立Handler對象。
六、為什麼在有些時候子線程中是可以直接更新UI的:
這道面試題應該是本文中最難的一個面試題了,需要好好理解。為了回答這個問題,我們需要先通過看源碼去瞭解下面這三個問題:
(1)Android是如何檢測非UI線程去更新UI的
(2)ViewRootImp是什嗎?
(3)ViewRootImp是在哪裡建立的?
源碼我就不貼出來了,這裡我只是總結一下。
答案:
非UI線程真的不能更新UI嗎? 是可以的。
解釋:
線上程中更新UI時會調用ViewParent.invalidateChild()方法檢查當前的thread是否是Mainthread。
具體源碼如下:
1 final ViewParent p = mParent;2 if (p != null && ai != null && l < r && t < b) {3 final Rect damage = ai.mTmpInvalRect;4 damage.set(l, t, r, b);5 p.invalidateChild(this, damage);6 }
而ViewParent是一個介面類,其實作類別是ViewRootImpl,通過查看invalidateChild()方法裡面的代碼就可以看到會他調用checkThread()方法。checkThread()方法如下:
1 void checkThread() {2 if (mThread != Thread.currentThread()) { //檢查更新UI的線程是否是MainThread3 throw new CalledFromWrongThreadException(4 "Only the original thread that created a view hierarchy can touch its views.");5 }6 }
上面的第02行就是檢查:線上程中更新UI時當前線程是否是MainThread。
但是,ViewRootImpl這個類是在activity的onResume()方法中建立的。就算在子線程中更新UI,只要在ViewRootImpl建立之前更新UI(比如,程式在執行onCreate方法時,我就去執行setText方法區更新UI),就可以逃避掉checkThread()的檢查。
關於本題,給出以下連結大家去細讀一下源碼吧:
Android更新Ui進階精解(一):
http://www.jianshu.com/p/6de0a42a44d6
為什麼我們可以在非UI線程中更新UI:
http://blog.csdn.net/aigestudio/article/details/43449123
Android中關於Handler的若干思考