Android的UI是單線程的,所以對於已耗用時間長的程式必須非同步運行。實現非同步任務的一個很方便的工具是AsyncTask。它完全隱藏了運行任務的線程的很多詳細資料。 以一個例子來說明AsyncTask: 一個非常簡單的應用中,有需要初始化遊戲引擎,當載入內容時,顯示一些插播廣告圖形。假設,我們希望在使用者等待遊戲啟動時,顯示一個動畫背景(類似於Windows Phone 8)上的載入程式的等待介面。當使用者在點擊啟動按鈕以後,會執行多個初始化。 問題:如果在UI線程中執行遠程服務調用初始化時,整個UI介面無法執行任何其他動作。 解決:使用AsyncTask解決這個問題,代碼如下:
private final class AsyncInitGame extends AsyncTask{ private final View root; private final Game game; private final TextView message; private final Drawable bg; public AsyncInitGame(View root, Drawable bg, Game game, TextView msg) { this.root = root; this.bg = bg; this.game = game; this.message = msg; } //run on the UI thread //1. 當UI線程調用任務的execute方法時,會首先調用該方法,這裡要做的操作時該任務能夠對其本身和環境執行初始化,在這個例子中是安裝等待啟動的背景動畫 @Override protected void onPreExecute() { if(0 >= mInFlight++){ root.setBackgroundResouce(R.anim.dots); ((AnimationDrawable)root.getBackground()).start(); } } //runs on the UI thread //3. 當doInBackground方法完成時,就移除背景線程,再在UI線程中調用onPostExecute方法。 @Override protected void onPostExecute(String msg){ if(0 >= --mInFlight){ ((AndimationDrawable)root.getBackground()).stop(); root.setBackgroundDrawable(bg); } message.setText(msg); } //runs on a background thread //2. 在onPreExecute方法完成後AsyncTask建立新的背景線程,並發執行doInBackground方法。 @Override protected String doInBackground(String... args){ return (1 != args.length) || (null == args[0])) ? null : game.initialize(args[0]); }}private final class AsyncInitGame extends AsyncTask{ private final View root; private final Game game; private final TextView message; private final Drawable bg; public AsyncInitGame(View root, Drawable bg, Game game, TextView msg) { this.root = root; this.bg = bg; this.game = game; this.message = msg; } //run on the UI thread //1. 當UI線程調用任務的execute方法時,會首先調用該方法,這裡要做的操作時該任務能夠對其本身和環境執行初始化,在這個例子中是安裝等待啟動的背景動畫 @Override protected void onPreExecute() { if(0 >= mInFlight++){ root.setBackgroundResouce(R.anim.dots); ((AnimationDrawable)root.getBackground()).start(); } } //runs on the UI thread //3. 當doInBackground方法完成時,就移除背景線程,再在UI線程中調用onPostExecute方法。 @Override protected void onPostExecute(String msg){ if(0 >= --mInFlight){ ((AndimationDrawable)root.getBackground()).stop(); root.setBackgroundDrawable(bg); } message.setText(msg); } //runs on a background thread //2. 在onPreExecute方法完成後AsyncTask建立新的背景線程,並發執行doInBackground方法。 @Override protected String doInBackground(String... args){ return (1 != args.length) || (null == args[0])) ? null : game.initialize(args[0]); }} 假設AsyncTask的實現是正確的,我們單擊按鈕“啟動”按鈕只需要建立一個執行個體並調用它,如下所示:
((Button)findViewById(R.id.start)).setOnClickListener( new View.OnClickListener(){ @Override public void onClick(View v){ new AsyncInitGame(root, bg, game, msg).execute("basic"); } });//註:該文中的代碼並不全面,只是為了闡述AsyncTask doInBackground是類Game的代理(proxy)。解釋: 一般來說AsyncTask需要一組參數並返回一個結果。因為需要線上程之間傳遞該參數並返回結果,所以就需要一些握手機制用來確保執行緒安全性。 1. 通過參數傳遞調用execute方法來調用AsyncTask。 2. 當線程在後台執行時,這些參數最終通過AsyncTask機制傳遞給doInBackground方法的,doInBackground返回結果。 3. AsyncTask把該結果作為參數傳遞給doPostExecute方法,doPostExecute方法和最初的execute方法在同一個線程中運行。
AsyncTask不但會確保資料流安全也會確保型別安全。該抽象基類(AsyncTask)使用Java反省,使得實現能夠制定任務參數和結果的類型,下面以一個例子來說明:
public class AsyncDBReq extends AsyncTask{ @Override protected ResultSet doInBackground(PreparedStatement...q){ //implementation..... } @Override protected onPostExecute(ResultSet result){ //implementation }}public class AsyncHttpReq extends AsyncTask{ @Override protected HeepResponse doInBackground(HttpRequest.....req){ //implementaion..... } @Override protected void onPostExecute(HttpResponse result){ //implementaion }}第一個類,AsyncDBReq執行個體的execute方法參數是一個或者多個PreparedStatement變數。該類中AsyncDBReq執行個體的doInBackground方法會把這些PreparedStatement參數作為其參數,返回結果是ResultSet。onPostExecute方法會把該ResultSet作為其參數使用。第二個類也是如此。
AsyncTask的一個執行個體只能運行一次!!!,第二次執行execute方法會拋出IllegalStateException一場。所以每個任務調用都需要一個新的執行個體。
雖然AsyncTask簡化平行處理,但它有很強的限制約束條件且無法自動驗證這些條件。注意不要違反這些約束條件是非常有必須要的,對於這些約束條件,最明顯的是doInBackground方法,因為它是在另一個線程上執行的,只能引用範圍內的變數!!!這樣才是安全執行緒的OK痛點來了,如果實際使用中可能還是會發生下面兩個這樣的錯誤,所以,可能需要大量的練習來熟悉和理解了。案例一:
//易犯錯誤一、//....some clasint mCOunt;public void initButton1(Button button){ mCOunt = 0; button.setonClickListener( new View.OnClickListener(){ @SuppressWarnings("unchecked") @Override public void onCLick(View v){ new AsyncTask(){ @Override protected Void doInBackground(Void...args){ mCount++; //!!! not thread safe!! return null; } }.execute(); }});} 這裡在編譯時間不會產生編譯錯誤,也沒有運行警告,可能甚至在bug被觸發時也不會立即失敗,但該代碼絕對是錯誤的。有兩個不同的線程訪問變數mCount,而這兩個線程之間卻沒有執行同步。 鑒於這種情況,在本文的第一段代碼中的mInFlight的訪問執行同步時,你可能會感到奇怪。事實上它是正確的,AsyncTask約束會確保onPreExecute方法和onPostExecute方法在同一個線程中執行,即execute方法被調用的線程。和mCount不同,mInFlight只有一個線程訪問,不需要執行同步。
案例二:可能會導致最致命的的並發問題是在用完某個參數變數後,沒有釋放其引用。如下代碼:
//易犯錯誤二、public void initButton(Button button, final Map vals){ button.setOnClickListener( new View.OnClickListener(){ @Override public void onClick(View v){ new AsyncTask, Void, Void>(){ @Override protected Void doInBackground(Map...params){ //implementation, uses the params Map } }.execute(vals); vals.clear();//this is not thread safe!!!! }});}錯誤原因:initButton的參數valse被並發引用,卻沒有執行同步!!!當調用AsyncTask時,它作為參數傳遞給execute方法。syncTask架構可以確保當調用doInBackground方法時,該引用會正確地傳遞給後台線程。但是對於在initButton方法中所儲存並使用的vals引用,卻沒有辦法處理。調用vals.caear修改了在另一個線程上正在使用的狀態,但沒有執行同步。因此,不是安全執行緒的。最佳解決辦法:確保AsyncTask的參數是不可變的。如果這些參數不可變,類似String、Integer或只包含final變數的POJO對象,那麼它們都是安全執行緒的,不需要更多的操作。要保證傳遞給AsyncTask的可變對象是程式安全的唯一辦法是確保只有AsyncTask持有引用。在案例二中參數vals是傳遞給initButton方法,我們完全無法保證它不存在懸空的引用(dangline references)。即使刪掉代碼vals.clear,也無法保證該代碼正確,因為調用initButton方法的執行個體可能會儲存其參數map的引用,它最終傳遞的是參數vals。使該代碼正確的唯一方式是完全複製(深拷貝)map及其包含的對象!!!
最後還有AsyncTask還有一個方法沒有使用到:onProgressUpdate。作用:使長時間啟動並執行任務可以周期性安全地把狀態返回給UI線程。
用一個例子來說明並結束本文吧,哎,打字不容易啊,妹子的電腦上沒有eclipse,只能用editplus,木有智能提示傷不起的。該例子說明了如何使用onProgressUpdate方法實現進度條,向使用者顯示遊戲初始化進程還需要多久的時間。
public class AsyncTaskDemoWithProgress extends Activity{ private final class AsyncInit extends AsyncTask implements Game.InitProgressLIstener { private final View root; private final Game game; private final TextView message; private final Drawable bg; public AsyncInit(View root, Drawable bg, Game game, TextView msg){ this.root = root; this.bg = bg; this.game = game; this.message = msg; } //run on the UI thread //1. 當UI線程調用任務的execute方法時,會首先調用該方法 @Override protected void onPreExecute() { if(0 >= mInFlight++){ root.setBackgroundResouce(R.anim.dots); ((AnimationDrawable)root.getBackground()).start(); } } //runs on the UI thread //3. 當doInBackground方法完成時,就移除背景線程,再在UI線程中調用onPostExecute方法。 @Override protected void onPostExecute(String msg){ if(0 >= --mInFlight){ ((AndimationDrawable)root.getBackground()).stop(); root.setBackgroundDrawable(bg); } message.setText(msg); } //runs on its own thread //2. 在onPreExecute方法完成後AsyncTask建立新的背景線程,並發執行doInBackground方法。 @Override protected String doInBackground(String... args){ return (1 != args.length) || (null == args[0])) ? null : game.initialize(args[0]); } //runs on its UI thread @Override protected void onProgressUpdate(Integer... vals){ updateProgressBar(vals[0].intValue()); } //runs on the UI thread @Override public void onInitProgress(int pctComlete){ //為了正確地給UI線程發布進程狀態,onInitProgress調用的是AsyncTask的publicProgress。 //AsyncTask處理UI線程的publicProgress調度細節,從而onProgressUpdate可以安全地使用View方法。 publicProgress(Integer.valueOf(pctComplete)); } } int mInFlight,mComplete; /** @see android.app.Activity#onCreate(android.os.Bundle) */ @Override public void onCreate(Bundle state){ super.onCreate(state); setContentView(R.layout.asyncdemoprogress); final View root = findViewById(R.id.root); final Drawable bg = root.getBackground(); final TextView msg = ((TextView)findViewById(R.id.msg)); final Game game = Game.newGame(); ((Button)findViewById(R.id.start)).setOnClickListener( new View.OnClickListener(){ @Override public void onCLick(View v){ mComplete=0; new AsyncInit(root, bg, game, msg).execute("basic"); }}); } void updateProgressBar(int progress){ int p = progress; if(mComplete < p){ mComplete = p; (ProgressBar)findViewById(R.id.progress)).setprogress(p); } }}.......嗯,其實還沒結束,來個總結:
· Android UI線程是單線程的。為了熟練使用Android UI,開發人員必須對任務隊列概念很熟悉。
· 為了保證UI的及時響應,需要啟動並執行任務的執行時間超過幾毫秒,或者需要好幾百條指令,都不應該在UI線程中執行。
· 並發編程很棘手,容易犯錯,並難以找出錯誤。
· AsyncTask是運行簡單、非同步任務的很便捷的工具。要記住的是doInBackground運行在另一個線程上。它不能寫任何其他線程可見的狀態,也不能讀任何其他線程可寫的狀態,這也包括其參數!
· 不可改變的對象時在併線程之間傳遞資訊的重要工具。
Mr.傅:學習筆記
歡迎轉載,轉載註明出處,謝謝《Android程式設計》引用說明:Programming Android by Zigurd Mednieks, Laird Dornin, G.Blake Meike, and Masumi Nakamura. Copyright 2011 O'Reilly Media, Inc., 978-1-449-38969-7