Android的程序與執行緒(3)執行緒安全問題
當一個程式啟動的時候,系統會為程式建立一個名為main的執行緒。這個執行緒重要性在於它負責把事件分發給適合的使用者元件,這些事件包括繪製事件。並且這個執行緒也是你的程式與Android UI工具包中的元件(比如android.widget和android.view包中的元件)進行互動的執行緒。正因為如此,這個main執行緒有時也被稱為UI執行緒。
系統並不會為元件的每個例項都建立一個單獨的執行緒。執行在同一個程序中的所有元件都是在UI執行緒中例項化的,並且系統對這些元件的呼叫都是由UI分發的。所以,對系統的回撥做出迴應的方法都是執行在程序中的UI執行緒中的(比如,用於報告使用者操作的onKeyDown()
比如,當用戶按下了螢幕上的一個按鈕,你程式的UI執行緒就會將這個觸屏事件分發給這個button元件,然後這個元件會依次設定它的按下狀態,並向事件佇列傳送一個無效請求(post an invalidate request to the event queue)。然後UI執行緒將這個請求彈出佇列,並通知這個元件使其重繪。
當你的程式在為了響應使用者事件,而頻繁的執行操作的時候,除非你能很恰當的實現你的程式,否則的話,可能會給使用者帶來不好的體驗。特別是,如果讓程式中所有的工作都在主執行緒中完成的時候,像訪問網際網路或者資料庫這些需要長時間的操作將會堵塞整個UI。當UI
另外,Android的UI toolkit不是執行緒安全的。所以,你不能在你的其他執行緒中處理UI,你的所有的UI方面的工作都應該在UI執行緒來完成。這裡,有兩條關於Android單執行緒模型的規則:
1.不要堵塞UI執行緒
2.不要在UI執行緒之外訪問Android UI toolkit
worker thread
正是因為上面討論的這種單執行緒模型,為了程式的響應速度,你是絕不能堵塞UI執行緒的。如果你要做的工作不是瞬時就能完成的,那麼你就應該在新的執行緒("background"或者“worker”執行緒)中處理它們。
比如,下面是關於單擊一個監聽器來在一個單獨的執行緒中下載一張圖片,並使用ImageView顯示該圖片的程式碼:
public void onClick(View v){
new Thread(new Runnable(){
Bitma b = loadImageFromNetwork("http://examble.com/image.png");
mImageView.setImageBitmap(b);
}).start();
}
乍看起來的話,這段程式碼好像是沒問題的,它建立一個新的執行緒來處理聯網操作。然而它違反了上面的第二條規則:不能在UI執行緒之外訪問Android Ui tookit,因為這個例子中在新的worker執行緒中修改了ImageView,而不是在UI執行緒中。這樣做將會導致未定義或者意料不到的行為發生,這對於發現這個錯誤來說,將是很難很耗時的。
為了解決這個問題,Android提供如下的方式,來實現從其他執行緒中訪問UI執行緒。下面是具體方法:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
比如,你可以使用View.post(Runnable)來修復上面的程式碼:
public void onClick(View v){
new Thread(new Runnable(){
public void run(){
final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
mImageView.post(new Runnable(){
public void run(){
mImageView.setImageBitmap(bitmap);
}
});
}
}).start();
}
現在這種實現方式,就是執行緒安全的了:聯網的操作在一個單獨執行緒中完成,而ImageView的處理是在UI執行緒中完成的。
然而,當隨著操作的複雜性提高的時候,上面的程式碼可能會變得複雜,並且不容易維護。為了使用worker執行緒處理更復雜的操作,你可以考慮在你的worker執行緒使用Handler來處理從UI執行緒中分發過來的訊息。或許最好的方式,是通過繼承AsyncTask這個類,這簡化了worker執行緒需要同UI進行互動的過程。下面介紹怎樣使用AsyncTask。
AsyncTask允許你對於UI執行非同步的操作。它在worker執行緒中執行耗時的操作,然後在UI執行緒中更新結果,在這個過程中不需要你去處理執行緒或者handlers。
為了使用AsyncTask,你必須繼承AsyncTask並實現doInBackground回撥方法,這些回撥方法執行在後臺執行緒的池中(run in a pool of background threads)。為了更新你的UI,你應該實現onPostExecute()方法,這個方法將doInBackground()方法中的處理結果傳遞到UI執行緒中,這樣,你就可以安全的更新你的UI了。然後你就可以在UI執行緒中呼叫execute()方法來實現這整個過程了。
比如,你可以通過使用AsyncTask來修改上面的程式碼:
public void onClick(View v){
new DownloadImageTask().execute("http://example.com/image.png");
}
private class DownloadImageTask extends AsyncTask<String, Void, Bitmap>{
/**The sytem calls this to perform work in worker thread and delivers it the parameters given to AsyncTask.execute()*/
procted Bitmap doInBackground(String... urls){
return loadImageFromNetwork(urls[0]);
}
/**系統呼叫該方法來在更新UI執行緒,並將doInbackground()的結果返回出來*/
protected void onPostExecute(Bitmap result){
mImageView.setImageBitmap(result);
}
}
現在這個Ui是安全的,並且程式碼更簡單。下面是對AsyncTask的一個簡單介紹:
-你可以通過使用泛型,來指定引數的型別,進度值(the progress values)和the final value of the task。
-方法doInBackground()將自動在worker執行緒中執行
-onPreExecute(),onPostExecute()和onProgressUpdate()方法都是在UI執行緒中觸發的
-doInBackground()方法的結果將返回到onPostExecute()方法中
-在doInbackground()方法中,你可以在任意的時間通過呼叫publishprogress()來在主執行緒中執行onProgressUpdate()
-你可以從任何執行緒中取消當前的task
這裡有一點需要注意的是當使用worker執行緒時,可能會因為裝置執行時配置發生了改變(比如螢幕翻轉),而導致worker執行緒重啟。