當用戶輸入城市名稱,然后單擊按鈕進行查詢后,程序會調用Google API的接口獲得指定城市的當日天氣情況。由于需要訪問網絡,所以當網絡出現異?;蛘叻辗泵Φ臅r候都會使訪問網絡的動作很耗時。本文為了 要演示超時的現象,只需要制造一種網絡異常的狀況,最簡單的方式就是斷開網絡連接,然后啟動該程序,同時觸發一個用戶事件,比如按一下MENU鍵, 由于主線程因為網絡異常而被長時間阻塞,所以用戶的按鍵事件在5秒 鐘內得不到響應,Android會 提示一個程序無法響應的異常,如下圖:
該對話框會詢問用戶 是繼續等待還是強行退出程序。當你的程序需要去訪問未知的網絡的時候都會可能會發生類似的超時的情況,用戶的響應得不到及時的回應會大大的降低用戶體驗。 所以我們需要參試以別的方式來實現
2.1 子線程更新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的單線程模型的原 則。
2.3 AsyncTask
雖然借助消息隊列已經可以較為完美的實現了天氣預報的功能,但是你還是不得不自己管理子線程,尤其當你的需要有一些復雜的邏輯以及需要頻繁的更新UI的時候,這樣的方式使得你的代碼難以閱讀和理解。
幸運的是Android另外提供了一個工具類:AsyncTask。它使得UI thread的使用變得異常簡單。它使創建需要與用戶界面交互的長時間運行的任務變得更簡單,不需要借助線程和Handler即可實現。
1) 子類化AsyncTask
2) 實現AsyncTask中定義的下面一個或幾個方法
? onPreExecute(), 該方法將在執行實際的后臺操作前被UI thread調用??梢栽谠摲椒ㄖ凶鲆恍蕚涔ぷ鳎缭诮缑嫔巷@示一個進度條。
? doInBackground(Params...), 將在onPreExecute 方法執行后馬上執行,該方法運行在后臺線程中。這里將主要負責執行那些很耗時的后臺計算工作。可以調用publishProgress方法來更新實時的任務進度。該方法是抽象方法,子類必須實現。
? 3. onProgressUpdate(Progress...),在publishProgress方 法被調用后,UI thread將調用這個方法從而在界面上展示任務的進展情況,例如通過一個進度條進行展示。
? 4. onPostExecute(Result), 在doInBackground 執行完成后,onPostExecute 方法將被UI thread調用,后臺的計算結果將通過該方法傳遞到UI thread.
為了正確的使用AsyncTask類,以下是幾條必須遵守的準 則:
1) Task的實例 必須在UI thread中創建
2) execute方 法必須在UI thread中調用
3) 不要手動的調用onPreExecute(), onPostExecute(Result),doInBackground(Params...), onProgressUpdate(Progress...)這幾個方法
4) 該task只能被執行一次,否則多次調用時將會出現異常
下面我們將通過AsyncTask并且嚴格遵守上面的4條準則來改寫天氣預報的例子:
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);
}
public void onClick(View v) {
//獲得用戶輸 入的城市名稱
String city = editText.getText().toString();
//必須每次都 重新創建一個新的task實例進行 查詢,否則將提示如下異常信息
//the task has already been executed (a task can be executed only once)
new GetWeatherTask().execute(city);
}
class GetWeatherTask extends AsyncTask<String, Integer, String> {
@Override
protected String doInBackground(String... params) {
String city = params[0];
//調用Google 天氣API查詢指定 城市的當日天氣情況
return getWetherByCity(city);
}
protected void onPostExecute(String result) {
//把doInBackground處理的結果 即天氣信息顯示在title上
setTitle(result);
}
}
注意這行代 碼:new GetWeatherTask().execute(city); 值得一提的是必須每次都重新創建一個新的GetWeatherTask來執行后臺任務,否則Android會提示“a task can be executed only once”的錯誤信息。
經過改寫后的 程序不僅顯得非常的簡潔,而且還減少了代碼量,大大增強了可讀性和可維護性。因為負責更新UI的onPostExecute方 法是由UI thread調用,所以沒有違背單線程模型的原則。良好的AsyncTask設計大大降低了我們犯錯誤的幾率。
5綜述
本文首先大致介紹了Android的單線程模型及其原則。然后通過一個真實案例展示剛接觸Android的 開發人員在不理解Android的 單線程模型下容易犯的錯誤。最后通過幾種正確的方式實現該案例,進一步認識和理解Android的單線程模型及其原則。由于更多地關注線程模型,本文或許不足以幫助讀者全面的認識Android技 術,關于文中提到的其他技術細節以及Android的 其他相關技術可以訪問Android的 官方網站進行進一步的了解和學習。