標籤:
為什麼需要線程
假設需要開發一個連網應用程式,需要從一個網址抓取網頁內容,這裡讀取的網頁地址是筆者在本地機器上自己建立的伺服器位址。當然在讀取網頁內容的時候,可以使用HttpClient提供的API,但是這並不是本文的介紹重點。缺乏連網程式開發經驗的程式員可能寫出下面的代碼。
package com.ophone.network;//這裡為了節省篇幅,忽略了import項public class NetworkActivity extends Activity { // 顯示任務的執行狀態和返回結果 private TextView message; private Button open; private EditText url; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); message = (TextView) findViewById(R.id.message); url = (EditText) findViewById(R.id.url); open = (Button) findViewById(R.id.open); open.setOnClickListener(new View.OnClickListener() { public void onClick(View arg0) { connect(); } }); } private String connect() { try { HttpClient client = new DefaultHttpClient(); // params[0]代表串連的url HttpGet get = new HttpGet(url.getText().toString()); HttpResponse response = client.execute(get); HttpEntity entity = response.getEntity(); long length = entity.getContentLength(); InputStream is = entity.getContent(); String s = null; if (is != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[128]; int ch = -1; int count = 0; while ((ch = is.read(buf)) != -1) { baos.write(buf, 0, ch); count += ch; // 為了在模擬器中清楚地看到進度,讓線程休眠1000ms Thread.sleep(50000); } s = new String(baos.toByteArray()); } // 返回結果 return s; } catch (Exception e) { e.printStackTrace(); } return null; }}
網路連接通常是比較耗時的,尤其是在當前的GPRS這種低速率的網路情況下,這樣connect()方法可能需要3-5秒,
甚至更長的時間才能返回頁面的內容。如果此串連動作直接在主線程,也就是UI線程中處理,會發生什麼情況呢?
為了在模擬器中更好的類比網路讀取速度慢的情況,
筆者在讀取過程中讓線程休眠了50秒,
運行NetworkActivity,點擊“串連”按鈕。意外發生了,
按鈕長時間沒有反應,整個介面似乎是“死”掉了。系統隨後顯示出了 ANR(應用程式無響應)
線上程中連網
為什麼出現ANR?答案是連網動作阻塞在了主線程,長時間沒有返回,這樣OPhone彈出ANR錯誤。這個錯誤提示我們,
如果否個任務可能需要長時間的運行才能返回,則必須把這個任務放置到單獨線程中運行,
避免阻塞UI線程。Java語言內建了對線程的支援,可以使用Thread類建立一個新線程,然後在run()方法中讀取網頁的內容,
獲得頁面內容後調用TextView.setText()更新介面。修改後的connect()
方法如下所示:
private void connect() { new Thread() { public void run() { try { HttpClient client = new DefaultHttpClient(); // params[0]代表串連的url HttpGet get = new HttpGet(url.getText().toString()); HttpResponse response = client.execute(get); HttpEntity entity = response.getEntity(); long length = entity.getContentLength(); InputStream is = entity.getContent(); String s = null; if (is != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[128]; int ch = -1; int count = 0; while ((ch = is.read(buf)) != -1) { baos.write(buf, 0, ch); count += ch; } s = new String(baos.toByteArray()); message.setText(s); } } catch (Exception e) { e.printStackTrace(); } } }.start(); }
重新運行NetworkActivity,點擊“串連”按鈕。程式並沒有像預期的那種獲得網頁的內容,並顯示到TextView上。查看log可以看到在connect的執行過程中拋出了異常。接下來分析問題的所在。
使用Handler更新介面
其實,connect()方法中拋出的異常是由於介面更新引起的。Connect()方法直接在新啟動的線程中調用message.setText()方法是不正確的。OPhone平台只允許在主線程中調用相關View的方法來更新介面。如果返回結果在新線程中獲得,那麼必須藉助Handler來更新介面。為此,在NetworkActivity中建立一個Handler對象,並在handleMessage()中更新UI。
//Task在另外的線程執行,不能直接在Task中更新UI,因此建立了Handler private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { String m = (String) msg.obj; message.setText(m); } };
當從connect()方法中獲得網頁內容後,使用如下方法更新介面。
s = new String(baos.toByteArray()); Message mg = Message.obtain(); mg.obj = s; handler.sendMessage(mg);
重新運行NetworkActivity,點擊“串連”按鈕,正確讀取了網頁的內容。
AsyncTask
看上去修改後的connect()方法已經可用了,但是這種匿名程的方式是存在缺陷的:第一,線程的開銷較大,如果每個任務都要建立一個線程,那麼應用程式的效率要低很多;第二,線程無法管理,匿名線程建立並啟動後就不受程式的控制了,如果有很多個請求發送,那麼就會啟動非常多的線程,系統將不堪重負。另外,前面已經看到,在新線程中更新UI還必須要引入handler,這讓代碼看上去非常臃腫。
為瞭解決這一問題,OPhone在1.5版本引入了AsyncTask。AsyncTask的特點是任務在主線程之外運行,而回調方法是在主線程中執行,這就有效地避免了使用Handler帶來的麻煩。閱讀AsyncTask的源碼可知,AsyncTask是使用java.util.concurrent 架構來管理線程以及任務的執行的,concurrent架構是一個非常成熟,高效的架構,經過了嚴格的測試。這說明AsyncTask的設計很好的解決了匿名線程存在的問題。
AsyncTask是抽象類別,子類必須實現抽象方法doInBackground(Params... p) ,在此方法中實現任務的執行工作,比如串連網路擷取資料等。通常還應該實現onPostExecute(Result r)方法,因為應用程式關心的結果在此方法中返回。需要注意的是AsyncTask一定要在主線程中建立執行個體。AsyncTask定義了三種泛型型別 Params,Progress和Result。
- Params 啟動任務執行的輸入參數,比如HTTP請求的URL。
- Progress 背景工作執行的百分比。
- Result 後台執行任務最終返回的結果,比如String。
- AsyncTask的執行分為四個步驟,與前面定義的TaskListener類似。每一步都對應一個回調方法,需要注意的是這些方法不應該由應用程式調用,開發人員需要做的就是實現這些方法。在任務的執行過程中,這些方法被自動調用。
- onPreExecute() 當任務執行之前開始調用此方法,可以在這裡顯示進度對話方塊。
- doInBackground(Params...) 此方法在後台線程執行,完成任務的主要工作,通常需要較長的時間。在執行過程中可以調用publicProgress(Progress...)來更新任務的進度。
- onProgressUpdate(Progress...) 此方法在主線程執行,用於顯示任務執行的進度。
- onPostExecute(Result) 此方法在主線程執行,任務執行的結果作為此方法的參數返回。
PageTask擴充了AsyncTask,在doInBackground()方法中讀取網頁內容。PageTask的原始碼如下所示:
// 設定三種型別參數分別為String,Integer,String class PageTask extends AsyncTask<String, Integer, String> { // 可變長的輸入參數,與AsyncTask.exucute()對應 @Override protected String doInBackground(String... params) { try { HttpClient client = new DefaultHttpClient(); // params[0]代表串連的url HttpGet get = new HttpGet(params[0]); HttpResponse response = client.execute(get); HttpEntity entity = response.getEntity(); long length = entity.getContentLength(); InputStream is = entity.getContent(); String s = null; if (is != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[128]; int ch = -1; int count = 0; while ((ch = is.read(buf)) != -1) { baos.write(buf, 0, ch); count += ch; if (length > 0) { // 如果知道響應的長度,調用publishProgress()更新進度 publishProgress((int) ((count / (float) length) * 100)); } // 為了在模擬器中清楚地看到進度,讓線程休眠100ms Thread.sleep(100); } s = new String(baos.toByteArray()); } // 返回結果 return s; } catch (Exception e) { e.printStackTrace(); } return null; } @Override protected void onCancelled() { super.onCancelled(); } @Override protected void onPostExecute(String result) { // 返回HTML頁面的內容 message.setText(result); } @Override protected void onPreExecute() { // 任務啟動,可以在這裡顯示一個對話方塊,這裡簡單處理 message.setText(R.string.task_started); } @Override protected void onProgressUpdate(Integer... values) { // 更新進度 message.setText(values[0]); } }
執行PageTask非常簡單,只需要調用如下代碼。重新運行NetworkActivity,不但可以抓取網頁的內容,還可以即時更新讀取的進度。讀者嘗試讀取一個較大的網頁,看看百分比的更新情況。
PageTask task = new PageTask();task.execute(url.getText().toString());
總結
本文介紹了OPhone連網應用開發中應該注意的兩個問題:線程管理和介面更新。不但分析了問題的所在,也給出了多種解決方案。這裡筆者推薦使用AsyncTask處理連網,播放大尺寸媒體檔案等較為耗時的工作,不但執行效率高,也可以節省代碼。
Android 開發筆記 “線程互動(Handler+Thread 和 AsyncTask)”