子線程更新UI
顯然假如你的程式需要執行耗時的操作的話,假如像上例一樣由主線程來負責執行該操作是錯誤的。所以我們需要在onClick方法中建立一個新的子線程來負責調用GOOGLE API來獲得天氣資料。剛接觸Android的開發人員最輕易想到的方式就是如下:
public void onClick(View v) { //建立一個子線程執行耗時的從網路上獲得天氣資訊的操作 new Thread() { @Override public void run() { //獲得使用者輸入的城市名稱 String city = editText.getText().toString(); //調用Google 天氣API查詢指定城市的當日天氣情況 String weather = getWetherByCity(city); //把天氣資訊顯示在title上 setTitle(weather); } }.start(); }
但是很不幸,你會發現Android會提示程式由於異常而終止。為什麼在其他平台上看起來很簡單的代碼在Android上啟動並執行時候依然會出錯呢?假如你觀察LogCat中列印的日誌資訊就會發現這樣的錯誤記錄檔:
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
從錯誤資訊不難看出Android禁止其他子線程來更新由UI thread建立的試圖。本例中顯示天氣資訊的title實際是就是一個由UI thread所建立的TextView,所以參試在一個子線程中去更改TextView的時候就出錯了。這顯示違反了單執行緒模式的原則:Android UI操作並不是安全執行緒的並且這些操作必須在UI線程中執行
2.2 Message Queue
在單執行緒模式下,為瞭解決類似的問題,Android設計了一個Message Queue(訊息佇列),線程間可以通過該Message Queue並結合Handler和Looper組件進行資訊交換。下面將對它們進行分別介紹:
l Message Queue
Message Queue是一個訊息佇列,用來存放通過Handler發布的訊息。訊息佇列通常附屬於某一個建立它的線程,可以通過Looper.myQueue()得到當前線程的訊息佇列。Android在第一啟動程式時會預設會為UI thread建立一個關聯的訊息佇列,用來管理程式的一些上層組件,activities,broadcast receivers 等等。你可以在自己的子線程中建立Handler與UI thread通訊。
l Handler
通過Handler你發行就緒或者處理一個訊息或者是一個Runnable的執行個體。沒個Handler都會與唯一的一個線程以及該線程的訊息佇列管理。當你建立一個新的Handler時候,預設情況下,它將關聯到建立它的這個線程和該線程的訊息佇列。也就是說,假如你通過Handler發布訊息的話,訊息將只會發送到與它關聯的這個訊息佇列,當然也只能處理該訊息佇列中的訊息。
主要的方法有:
1) public final boolean sendMessage(Message msg)
把訊息放入該Handler所關聯的訊息佇列,放置在所有目前時間前未被處理的訊息後。
2) public void handleMessage(Message msg)
關聯該訊息佇列的線程將通過調用Handler的handleMessage方法來接收和處理訊息,通常需要子類化Handler來實現handleMessage。
l Looper
Looper扮演著一個Handler和訊息佇列之間通訊橋樑的角色。程式組件首先通過Handler把訊息傳送給Looper,Looper把訊息放入隊列。Looper也把訊息佇列裡的訊息廣播給所有的Handler,Handler接受到訊息後調用handleMessage進行處理。
1) 可以通過Looper類的靜態方法Looper.myLooper得到當前線程的Looper執行個體,假如當前線程未關聯一個Looper執行個體,該方法將返回空。
2) 可以通過靜態方法Looper. getMainLooper方法得到主線程的Looper執行個體
線程,訊息佇列,Handler,Looper之間的關係可以通過一個圖來展現:
在瞭解了訊息佇列及其相關組件的設計思想後,我們將把天氣預告的案例通過訊息佇列來重新實現:
private EditText editText; private Handler messageHandler; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); editText = (EditText) findViewById(R.id.weather_city_edit); Button button = (Button) findViewById(R.id.goQuery); button.setOnClickListener(this); //得到當前線程的Looper執行個體,由於當前線程是UI線程也可以通過Looper.getMainLooper()得到 Looper looper = Looper.myLooper(); //此處甚至可以不需要設定Looper,因為 Handler預設就使用當前線程的Looper messageHandler = new MessageHandler(looper); } @Override public void onClick(View v) { //建立一個子線程去做耗時的網路連接工作 new Thread() { @Override public void run() { //活動使用者輸入的城市名稱 String city = editText.getText().toString(); //調用Google 天氣API查詢指定城市的當日天氣情況 String weather = getWetherByCity(city); //建立一個Message對象,並把得到的天氣資訊賦值給Message對象 Message message = Message.obtain(); message.obj = weather; //通過Handler發布攜帶有天氣情況的訊息 messageHandler.sendMessage(message); } }.start(); } //子類化一個Handler class MessageHandler extends Handler { public MessageHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { //處理收到的訊息,把天氣資訊顯示在title上 setTitle((String) msg.obj); } }
通過訊息佇列改寫過後的天氣預告程式已經可以成功運行,因為Handler的handleMessage方法實際是由關聯有該訊息佇列的UI thread調用,而在UI thread中更新title並沒有違反Android的單執行緒模式的原則。