Android中關於Handler的若干思考

來源:互聯網
上載者:User

標籤:

本文包括與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的若干思考

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.