安全多線程
本文將要討論Android應用程序所使用的線程模型以及Android應用程序是怎樣通過產生工作線程來完成那些耗時的工作,而不是在主線程裡處理這些工作來保證UI的性能的。同時本文還會對 應用程序與主線程中的Android UI toolkit組件交互以及產生托管工作線程的API進行解釋。
Android UI線程
當一個應用程序運行時,Android系統自動的為該應用程序創建一個叫做“Main”的主線程。“Main”線程,也叫UI線程,是一個十分重要的線程,他負責把包括UI繪制在內的所有事件分發到適當的Widget(UI組件)上面。同時,它還是你的應用程序與Android UI toolkit中運行的組件進行交互的線程。
例如,我們在屏幕上對某一個按鈕進行了觸摸,主(UI)線程就會把這個事件分發給Button(button是一個widget),此時該按鈕就會變為被按下的狀態並且向事件隊列中發送一個重繪請求。主線程從隊列中取出該請求然後告訴按鈕重繪自己。(這樣按鈕就表現為被按下的狀態了)
除非你的應用正確的實現了,否則這樣的單線程模式可能使應用程序產生很壞的表現。特別是,如果把所有的事件都放在主線程中來處理,需要長時間來處理的事件比如網絡訪問或者數據庫查詢,會阻塞整個UI。此時,在那些耗時的操作結束之前UI都不會分發任何事件,包括繪制事件。從用戶的角度來看,應用程序就是掛掉了,不再響應了。更壞的是,如果應用程序UI線程被阻塞超過幾秒鐘(現在大概是5秒),用戶就會看到一個讓人不高興的“應用程序沒有響應”(ANR)對話框。
如果你想看看這樣到底有多壞,寫一個簡單的按鈕,並在其單擊事件中添加Thread.sleep(2000)這段代碼。此時按下按鈕後2秒鐘內按鈕都會保持按下的狀態。當發生這樣的事情時,用戶很容易認為這個應用程序是一個十分慢的應用。
總而言之,保證你的UI不被阻塞對於應用程序的響應來說是十分重要 的。如果有耗時的操作,你應該把這些操作放在另外的線程中進行處理(後台線程或者工作線程)。
下面是一個通過點擊下載圖片並顯示到ImageView中的例子:
public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
Bitmap b = loadImageFromNetwork();
mImageView.setImageBitmap(b);
}
}).start();
}
乍一看,上面的代碼似乎沒有什麼問題,因為他並沒有阻塞UI線程。但不幸的是,上面的代碼違背了UI線程的單線程模式:Android UI toolkit不是線程安全的,所以對UI的操作都應該在主UI線程中進行。在上面的代碼中ImageView被工作線程所操作了,這樣可能會導致十分奇怪的問題,尋找或者解決這樣的Bug都將十分困難且耗時。
Android提供了幾種不同的方式來通過其它線程訪問UI線程。其中的一些你可能已經見過,下面是一個比較完全的列表:
l Activity.runOnUiThread(Runnable) <http://developer.android.com/reference/android/app/Activity.html>
l View.post(Runnable) <http://developer.android.com/reference/android/view/View.html>
l View.postDelayed(Runnable, long) <http://developer.android.com/reference/android/view/View.html>
l Handler <http://developer.android.com/reference/android/os/Handler.html>
你可以使用任何一種方式來修正上面的代碼:
public void onClick(View v) {
new Thread(new Runnable() {
public void run() {
final Bitmap b = loadImageFromNetwork();
mImageView.post(new Runnable() {
public void run() {
mImageView.setImageBitmap(b);
}
});
}
}).start();
}
不幸的是,這些類和方法會使你的代碼更加的復雜且不容易理解。在某些需要頻繁使用UI的操作中這樣的操作將會更加的復雜和難以理解。
為了解決這樣的問題,Android在1.5以及以後的版本中提供了一個輔助類:AsyncTask。這個類簡化了創建需要與UI長時間交互的線程的過程。
在Android1.0和1.1中也有一個與AsyncTask相同的類,UserTask,它提供了與AsyncTask相同的API。
AsyncTask的目的是替你管理應用程序中的線程。前面的例子可以簡單的使用下面的代碼來完成:
public void onClick(View v) {
new DownloadImageTask().execute("http://example.com/image.png");
}
private class DownloadImageTask extends AsyncTask {
protected Bitmap doInBackground(String... urls) {
return loadImageFromNetwork(urls[0]);
}
protected void onPostExecute(Bitmap result) {
mImageView.setImageBitmap(result);
}
}
如您所見,我們必須通過繼承AsyncTask類來使用AsyncTask。同時,AsyncTask實例只能在UI線程中創建且只能執行一次。你可以通過閱讀AsyncTask documentation <http://developer.android.com/reference/android/os/AsyncTask.html> 來完全的了解AsyncTask類,下面是一個關於他怎樣工作的概述。
l 你可以定義AsyncTask的參數類型和最終的返回值類型。
l 方法doInBackground()會自動的在工作線程中執行。
l onPreExecute(),onPostExecute()以及onProgressUpdate()都是在主線程中調用的。
l doInBackground()的返回值被發送到onPostExecute()中。
l 你可以在doInBackground()的任何時間調用publishProgress()來在主線程執行onProgressUpdate().
l 你可以在任何時間,通過任何線程結束這個任務。
做為官方文件的補充,你可以在Shelves (ShelvesActivity.java <http://code.google.com/p/shelves/source/browse/trunk/Shelves/src/org/curiouscreature/android/shelves/activity/ShelvesActivity.java> and AddBookActivity.java <http://code.google.com/p/shelves/source/browse/trunk/Shelves/src/org/curiouscreature/android/shelves/activity/AddBookActivity.java>) 和Photostream(LoginActivity.java <http://code.google.com/p/apps-for-android/source/browse/trunk/Photostream/src/com/google/android/photostream/LoginActivity.java>, PhotostreamActivity.java <http://code.google.com/p/apps-for-android/source/browse/trunk/Photostream/src/com/google/android/photostream/PhotostreamActivity.java> and ViewPhotoActivity.java <http://code.google.com/p/apps-for-android/source/browse/trunk/Photostream/src/com/google/android/photostream/ViewPhotoActivity.java>)的源代碼中閱讀幾個復雜的例子。我們強烈推薦您閱讀 Shelves <http://code.google.com/p/shelves/> 的源碼來了解怎樣在配置改變的情況下保證任務執行以及怎樣在Activity被銷毀時正確的取消這些任務。
無論您是否使用 AsyncTask <http://developer.android.com/reference/android/os/AsyncTask.html>,,都要記住單線程模型的兩個規則:
1.不要阻塞UI線程
2.保證只在UI線程中訪問Android UI toolkit.
AsyncTask類只是把上面的兩件事情變得簡單了。
Finish Whatever U've Started !!!