在以前的博文中,講過一些和Handler有關的知識,例如:html
Android 多線程----AsyncTask異步任務詳解 java
Android多線程----異步消息處理機制之Handler詳解android
今天再把Handler的知識回顧總結一下。面試
本文包括與Handler有關的如下問題:安全
(1)Handler的做用微信
(2)爲何Android中要設計爲只能在UI線程中去更新UI呢?多線程
(3)Handler的兩個異常架構
(4)Handler、Looper MessageQueue之間的關係(源碼角度)併發
(5)跟線程相關的Handler,即HandlerThread(源碼角度分析)app
(6)主線程往子線程發消息
1、Handler的做用:
(1)在非UI線程中完成耗時操做,在UI線程中去更新UI。
(2)能夠在主線程中發送延時消息。
2、爲何Android中要設計爲只能在UI線程中去更新UI呢?
(1)解決多線程併發問題(根本緣由)
(2)提升界面更新的性能問題
(3)架構設計的簡單
你可能會說,既然是擔憂多線程併發問題,那我在子線程中加鎖進行更新UI行不行呢?你這樣想的話,會容易形成UI卡頓的,並且性能也很差。
注1:大部分面試者很難去說出一個令面試官滿意的答案。
注2:關於多線程,這裏舉一個例子,好比說銀行取款的問題。正常狀況下,銀行卡餘額不能少於取款金額,若是多線程進行取款的話,就會形成線程不安全。
注3:Android中之因此說架構簡單,是由於幫咱們封裝了不少更新UI的操做。
3、Handler的兩個異常:
在使用Handler時,常常會出現如下兩個異常:
(1)CalledFromWrongThreadException:這種異常是由於嘗試在子線程中去更新UI,進而產生異常。
(2)Can't create handle inside thread that ha not called Looper.prepared:是由於咱們在子線程中去建立Handler,而產生的異常。
咱們接下來經過代碼來把這兩個異常演示一下。
一、子線程中更新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 @Override 13 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 //點擊按鈕後,嘗試在子線程中更新UI 20 btn.setOnClickListener(new View.OnClickListener() { 21 @Override 22 public void onClick(View v) { 23 24 new Thread(new Runnable() { 25 @Override 26 public void run() { 27 tv.setText("smyhvae"); //子線程中更新UI 28 } 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 //程序一加載,直接在主線程中建立Handler 16 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 @Override 29 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 @Override 38 public void onClick(View v) { 39 switch (v.getId()) { 40 case R.id.change_text: 41 new Thread(new Runnable() { 42 @Override 43 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();
二、在子線程中建立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 @Override 11 protected void onCreate(Bundle savedInstanceState) { 12 super.onCreate(savedInstanceState); 13 setContentView(R.layout.activity_main); 14 tv = (TextView) findViewById(R.id.tv); 15 16 //嘗試在子線程中去建立Handler 17 new Thread(new Runnable() { 18 @Override 19 public void run() { 20 new Handler(); 21 } 22 }).start(); 23 } 24 }
運行程序後, 報錯以下:
4、Handler、Looper MessageQueue之間的關係:(源碼角度)
若是要問到Handler,這個問題基本是面試必問的。
原理分析:
Handler是Android類庫提供的用於發送、處理消息或Runnable對象的處理類,它結合Message、MessageQueue和Looper類以及當前線程實現了一個消息循環機制,用於實現任務的異步加載和處理。整個異步消息處理流程的示意圖以下圖所示:
根據上面的圖片,咱們如今來解析一下異步消息處理機制:
如今能夠作出以下總結:
(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之間的關係,並不可以真正打動面試官,倒不如再舉一個生動的例子,讓面試官以爲你是懂面向對象的思惟的。
5、跟線程相關的Handler,即HandlerThread(源碼角度分析)
這個問題能夠看一下這篇博客:
http://blog.csdn.net/lmj623565791/article/details/47079737
6、主線程往子線程發消息:(考察你是否真的理解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 @Override 19 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方法裏面會報空指針? 26 new Thread(new Runnable() { 27 @Override 28 public void run() { 29 //一、準備Looper對象 30 Looper.prepare(); 31 //二、在子線程中建立Handler 32 handler = new Handler() { 33 @Override 34 public void handleMessage(Message msg) { 35 super.handleMessage(msg); 36 Log.i("handleMessage:", Thread.currentThread().getName()); 37 Log.i("後臺輸出", "收到了消息對象"); 38 } 39 }; 40 //三、調用Looper的loop()方法,取出消息對象 41 Looper.loop(); 42 } 43 }).start(); 44 45 } 46 @Override 47 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中發送消息,在子線程中接收消息的固定寫法。上面的三個步驟再重複一下:
注意,此時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對象。
6、爲何在有些時候子線程中是能夠直接更新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的線程是不是MainThread 3 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
下圖是個人微信公衆號(生命團隊id:vitateam
),歡迎有心人關注。博客園分享技術,公衆號分享心智。
我會很感激第一批關注個人人。此時,年輕的我和你,一無全部;然後,富裕的你和我,滿載而歸。