天天看點

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線程被阻塞的時候,所有的事件都不能被分發,包括繪制事件。從使用者的角度來講,這時這個程式就像終止了一樣。更糟糕的是,如果UI線程被堵塞超過5秒鐘的話,系統将會彈出一個“application not responing”(ANR)對話框。然後使用者可能就會決定放棄你的這個程式甚至選擇解除安裝。

另外,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線程重新開機。

參考文檔:

http://developer.android.com/guide/topics/fundamentals/processes-and-threads.html