1
引言
Android一詞本義指機器人,Google於2007年11月發佈了以Android命名的開源移動設備綜合平臺,包括其基於
Linux的操作系統、中間件和關鍵的手機應用。並且組建了開放手機聯盟,其成員囊括了全球著名的各大手機生產
商和移動運營商。2008年8月,Google又發佈了網上應用商店Android Market。任何一個開發者只需要藉助Android發
布的SDK開發手機應用,即可把開發的應用在Android Market上銷售。目前Android
Market上已經有一萬多的應用程序,大大豐富了Android手機用戶的功能。一個完整的產業鏈已經形成。因此開源Android吸引了原來越多的開
發人員加入進來。本文將跟讀者一起學習Android的線程模型。
2 Android進程
在瞭解Android線程
之間得先了解一下Android的進程。當一個程序第一次啓動的時候,Android會啓動一個LINUX進程和一個主線程。默認的情況下,所有該程序的
組件都將在該進程和線程中運行。同時,Android會爲每個應用程序分配一個單獨的LINUX用戶。Android會勁量保留一個正在運行進程,只在內
存資源出現不足時,Android會參試停止一些進程從而釋放足夠的資源給其他新的進程使用,
也能保證用戶正在訪問的當前進程有足夠的資源去及時的響應用戶的事件。Android會
根據進程中運行的組件類別以及組件的狀態來判斷該進程的重要性,Android會 首先停止那些不重要的進程。按照重要性從高到低一共有五個級別:
l
前臺進程
前臺進程是用戶當前正在使用的進程。只有一些前臺進程可以在任何時候都存在。他們是最後一個被結束的,當內存低到根本連他們
都不能運行的時候。一般來說, 在這種情況下,設備會進行內存調度,中止一些前臺進程來保持對用戶交互的響應。
l 可見進程
可
見進程不包含前臺的組件但是會在屏幕上顯示一個可見的進程是的重要程度很高,除非前臺進程需要獲取它的資源,不然不會被中止。
l
服務進程
運行着一個通過startService()
方法啓動的service,這個service不屬於上面提到的2種更高重要性的。service所在的進程雖然對用戶不是直接可見的,但是他們執行了用
戶非常關注的任務(比如播放mp3,從網絡下載數據)。只要前臺進程和可見進程有足夠的內存,系統不會 回收他們。
l 後臺進程
運
行着一個對用戶不可見的activity(調用過 onStop() 方法).這些進程對用戶體驗沒有直接的影響,可以在服務進程、可見進程、前臺進
程需要內存的時候回收。通常,系統中會有很多不可見進程在運行,他們被保存在LRU (least recently used)
列表中,以便內存不足的時候被第一時間回收。如果一個activity正 確的執行了它的生命週期,關閉這個進程對於用戶體驗沒有太大的影響。
l
空進程
未運行任何程序組件。運行這些進程的唯一原因是作爲一個緩存,縮短下次程序需要重新使用的啓動時間。系統經常中止這些進程,這
樣可以調節程序緩存和系統緩 存的平衡。
Android
對進程的重要性評級的時候,選取它最高的級別。另外,當被另外的一個進程依賴的時候,某個進程的級別可能會增高。一個爲其他進程服務的進程永遠不會比被服
務的進程重要級低。因爲服務進程比後臺activity進程重 要級高,因此一個要進行耗時工作的activity最好啓動一
個service來做這個工作,而不是開啓一個子進程――特別 是這個操作需要的時間比activity存在的時間還要長的時
候。例如,在後臺播放音樂,向網上上傳攝像頭拍到的圖片,使用service可
以使進程最少獲取到“服務進程”級別的重要級,而不用考慮activity目 前是什麼狀態。broadcast
receivers做費時的工作的時候,也應該啓用一個服務而不是開一個線程。
2單線程模型
當一個程序第一次啓動時,Android會同時啓動一個對應的 主線程(Main
Thread),主線程主要負責處理與UI相關的事件,如:用戶的按鍵事件,用戶接觸屏幕的事件以及屏幕繪圖事
件,並把相關的事件分發到對應的組件進行處理。所以主線程通常又被叫做UI線 程。在開發Android應用時必須遵守單線程模型的原則:
Android UI操作並不是線程安全的並且這些操作必須在UI線程中執行。
2.1 案例演示
如果在沒有理解這樣
的單線程模型的情況下,設計的程序可能會使程序性能低下,因爲所有的動作都在同一個線程中觸發。例如當主線程正在做一些比較耗時的操作的時候,如正從網絡
上下載一個大圖片,或者訪問數據庫,由於主線程被這些耗時的操作阻塞住,無法及時的響應用戶的事件,從用戶的角度看會覺得程序已經死掉。如果程序長時間不
響應,用戶還可能得重啓系統。爲了避免這樣的情況,Android設 置了一個5秒 的超時時間,一旦用戶的事件由於主線程阻塞而超過5秒
鐘沒有響應,Android會 彈出一個應用程序沒有響應的對話框。下面將通過一個案例來演示這種情況:
本程序將設計和實現查看指定城
市的當天天氣情況的功能,
1. 首先,需要選擇一個天氣查詢的 服務接口,目前可供選擇的接口很多,諸如YAHOO的
天氣API和Google提 供的天氣API。 本文將選擇GOOGLE 的 天氣查詢API。
該接口提供了多種查詢方式,可以通過指定具體城市的經緯度進行查詢,也可以通過城市名稱進行查詢。
2. 用戶在輸入框內輸入需要查詢的
城市名稱,然後點擊查詢按鈕
3. 當用戶點擊查詢按鈕後,使用已 經內置在Android SDK中的HttpClient
API來調用GOOGLE 的 天氣查詢API, 然後解析返回的指定城市的天氣信息,並把該天氣信息顯示在Title上
主要代碼如
下:
public class WeatherReport extends Activity implements
OnClickListener {
private static final String GOOGLE_API_URL
= "http://www.google.com/ig/api?weather=";
private static
final String NETWORK_ERROR = "網絡異常";
private EditText
editText;
@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);
}
@Override
public void onClick(View v) {
//獲得用戶輸入的城市名稱
String city = editText.getText().toString();
//調用Google
天氣API查詢指定城市的當日天氣 情況
String weather =
getWetherByCity(city);
//把天氣信息顯示在title上
setTitle(weather);
}
public String
getWetherByCity(String city) {
HttpClient httpClient =
new DefaultHttpClient();
HttpContext localContext = new
BasicHttpContext();
HttpGet httpGet = new
HttpGet(GOOGLE_API_URL + city);
try {
HttpResponse response = httpClient.execute(httpGet, localContext);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
httpGet.abort();
} else {
HttpEntity httpEntity = response.getEntity();
return parseWeather(httpEntity.getContent());
}
} catch (Exception e) {
Log.e("WeatherReport",
"Failed to get weather", e);
} finally {
httpClient.getConnectionManager().shutdown();
}
return NETWORK_ERROR;
}
}
當用戶輸入城市名稱,然後單擊按鈕進
行查詢後,程序會調用Google
API的接口獲得指定城市的當日天氣情況。由於需要訪問網絡,所以當網絡出現異常或者服務繁忙的時候都會使訪問網絡的動作很耗時。本文爲了
要演示超時的現象,只需要製造一種網絡異常的狀況,最簡單的方式就是斷開網絡連接,然後啓動該程序,同時觸發一個用戶事件,比如按一下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設計大大降低了我們犯錯誤的機率。
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
http://dev.10086.cn/cmdn/wiki/index.php?edition-view-4223-1.html