《Android》『Multi – Threads』- 實現多執行緒的三種方式 (Post、Handler、AsyncTask)
《Android Developers 參考文獻》
《簡單介紹》
在撰寫 Android 程式的過程中,常常會碰到一些需要快速更新 UI 介面的功能需求,這時候如果我們直接將更新 UI 的程式寫在主執行緒上,會發現 UI 一動也不動,直到所有運算處理完,才顯示最後的 UI 變動結果,若是運算的量再稍微大一點,甚至會產生 ANR(Application is Not Responding) 的對話框。
這是因為主執行緒會優先處理邏輯運算的關係,但是也只有主執行緒才能更新 UI,因此為了不讓畫面卡住,在處理這種需要大量更新 UI 的需求的時候,我們通常會透過新增子執行緒的方式來處理邏輯運算,並在處理完成的不同階段,再通知主執行緒更新 UI,避免程式出問題。
《程式範例》
這邊直接透過程式碼片段,來說明實現多執行緒的幾種方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/btANR" android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="onANRClick" android:text="ANR Click" /> <TextView android:id="@+id/tvANR" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ANR Click" /> <Button android:id="@+id/btUiThread" android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="onUiThreadClick" android:text="UI Thread Click" /> <TextView android:id="@+id/tvUiThread" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="UI Thread"/> <Button android:id="@+id/btViewPost" android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="onViewPostClick" android:text="View Post Click" /> <TextView android:id="@+id/tvViewPost" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="View Post"/> <Button android:id="@+id/btHandler" android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="onHandlerClick" android:text="Handler Click" /> <TextView android:id="@+id/tvHandler" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Handler"/> <Button android:id="@+id/btAsyncTask" android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="onAsyncTaskClick" android:text="AsyncTask Click" /> <ProgressBar android:id="@+id/progressBar" style="@android:style/Widget.ProgressBar.Horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:progress="0" /> <TextView android:id="@+id/tvAsyncTask" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="AsyncTask" /> </LinearLayout> |
main_activity.xml
首先我們先定義了一個包含五個按鈕的介面,分別代表不同的執行緒實作方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
package ... import ... public class MainActivity extends AppCompatActivity { . . . @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); tvANR = (TextView) findViewById(R.id.tvANR); tvUiThread = (TextView) findViewById(R.id.tvUiThread); tvViewPost = (TextView) findViewById(R.id.tvViewPost); tvHandler = (TextView) findViewById(R.id.tvHandler); tvAsyncTask = (TextView) findViewById(R.id.tvAsyncTask); progressBar = (ProgressBar) findViewById(R.id.progressBar); } /* Android will display the ANR dialog in the following conditions: 1. No response to an input event within 5 seconds. 2. A BroadcastReceiver hasn't finished executing within 10 seconds. */ public void onANRClick(View view) { Log.d(TAG, "onANRClick"); long count = 0; while (true) { count++; tvANR.setText(String.valueOf(count)); } } // only UI thread (main thread) can touch views public void onUiThreadClick(View view) { Log.d(TAG, "onUiThreadClick"); new Thread(new Runnable() { @Override public void run() { for (int count = 0; count < 10000; count++) { tvUiThread.setText(String.valueOf(count)); } } }).start(); } /* post() causes the Runnable to be added to the message queue. The runnable will be run on the user interface thread. */ public void onViewPostClick(View view) { Log.d(TAG, "onViewPostClick"); if (myThread == null) { myThread = new Thread(new Runnable() { int count; @Override public void run() { for (count = 0; count < 10000; count++) { try { Thread.sleep(100); } catch (InterruptedException e) { Log.e(TAG, e.toString()); } tvViewPost.post(new Runnable() { @Override public void run() { tvViewPost.setText(String.valueOf(count)); } }); } } }); myThread.start(); } } public void onHandlerClick(View view) { Log.d(TAG, "onHandlerClick"); if (myHandler == null) { myHandler = new MyHandler(tvHandler); } if (workThread == null) { workThread = new WorkThread(myHandler, 10000); workThread.start(); } } public void onAsyncTaskClick(View view) { Log.d(TAG, "onAsyncTaskClick"); if (myAsyncTask == null) { myAsyncTask = new MyAsyncTask(); myAsyncTask.execute(100); } } private class MyAsyncTask extends AsyncTask<Integer, Integer, String> { private ProgressDialog progressDialog; @Override protected void onPreExecute() { progressDialog = new ProgressDialog(MainActivity.this); progressDialog.setMessage("Loading..."); progressDialog.show(); } @Override protected String doInBackground(Integer... params) { int count = params[0]; for (int i = 0; i <= count; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { Log.e(TAG, e.toString()); } publishProgress(i); } return "Task completed!!"; } @Override protected void onProgressUpdate(Integer... values) { int progress = values[0]; if (progressDialog.isShowing()) { progressDialog.cancel(); } progressBar.setProgress(progress); tvAsyncTask.setText(String.valueOf(progress)); } @Override protected void onPostExecute(String result) { progressBar.setVisibility(View.GONE); tvAsyncTask.setText(result); } } } |
MainActivity.java
接著在 MainActivity 中,針對不同按鈕實作程式碼,相關說明如下 –
1. 在 onANRClick() 中,由於直接在主執行緒、也就是 UI 執行緒上做更新 tvANR 元件畫面的操作,主執行緒永遠卡在 While 的邏輯運算裡,邏輯運算無法結束,便無法更新 UI,最終導致 ANR 的結果。
2. 在 onUiThreadClick() 中,新增了一個子執行緒來執行邏輯運算並在更新 UI 畫面,此按鈕按下後程式會直接 Crash,因為在 Android 中,只有主執行緒可以更新 UI 畫面。
3. 在 onViewPostClick() 中,我們同樣新增一個子執行緒來執行邏輯運算,但在更新 UI 畫面的部分,我們透過 tvViewPost.post(new Runnable(){…}) 的方式,將其導回主執行緒來處理,在 post 方法中,其更新畫面的順序是先進先出,另外我們額外定義了 Thread.sleep(100) 的延遲,用以防止 UI 更新過快。
4. 在 onHandlerClick() 中,我們用到了兩個自訂的類別,分別是 MyHandler 與 WorkThread,MyHandler 透過建構子將需要更新 UI 介面的元件代入,並在其 handleMessage 中定義好接收到不同參數時所要執行的動作,WorkThread 則是一個繼承自 Thread 的自訂類別,在其中透過建構子將 MyHandler 物件傳入,再藉由 handler.obtainMessage(…) 的方式,來觸發 MyHandler 物件中的 handleMessage(),相關程式碼如下 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package ... import ... public class MyHandler extends Handler { private TextView textView; public MyHandler(TextView textView) { this.textView = textView; } @Override public void handleMessage(Message message) { switch (message.what) { case Common.MESSAGE_NORMAL: switch (message.arg1) { case Common.TASK_ADD: int sum = message.arg2; String worker = message.obj.toString(); String text = worker + ", sum = " + String.valueOf(sum); textView.setText(text); break; } break; case Common.MESSAGE_ERROR: textView.setText(message.obj.toString()); break; } } } . . . |
MyHandler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package ... import ... public class WorkThread extends Thread { private static final String TAG = "WorkThread"; private static final String WORKER = "John"; private int count; private Handler handler; public WorkThread(Handler handler, int count) { this.handler = handler; this.count = count; } @Override public void run() { for (int sum = 1; sum <= count; sum++) { try { Thread.sleep(100); } catch (InterruptedException e) { Log.e(TAG, e.toString()); handler.obtainMessage(Common.MESSAGE_ERROR, WORKER + " got " + e.toString()) .sendToTarget(); } handler.obtainMessage(Common.MESSAGE_NORMAL, Common.TASK_ADD, sum, WORKER) .sendToTarget(); } } } |
WorkThread.java
在這類別中,我們透過 handler.obtainMessage(int work, int arg1, int arg2, object obj) 方法,分別傳遞所需的資訊去觸發 MyHandler 中的 handleMessage 函式,其中第一個參數的意義代表要做什麼,第二至四個參數代表所可以夾帶的資訊,帶有一個 object 型別的參數以傳遞任何資料,這邊我們自訂了 Common 類別的參數來設定不同的執行內容,關於 Common 類別的定義如下。
1 2 3 4 5 6 7 8 9 10 |
package ... public class Common { public final static int MESSAGE_NORMAL = 0; public final static int MESSAGE_ERROR = 1; public final static int TASK_ADD = 0; public final static int TASK_SUBTRACT = 1; } |
Common.java
5. 在 onAsyncTaskClick() 中,我們透過繼承 AsyncTask<Integer, Integer, String> 自訂了一個 MyAsyncTask 類別,其中附帶的三個參數(Integer, Integer, String),分別對應到三個名為 doInBackground()、onProgressUpdate()、onPostExecute() 的 override 方法之傳入參數,透過 AsyncTask 本身的架構,我們可以定義在 Thread 執行前、執行期間以及執行完成後,該做些甚麼事。
其中要注意的是,myAsyncTask.execute(100) 方法代表將會傳入一個值為 100 的參數給 doInBackground() 所代入的參數,此參數可以不只一個, publishProgress(i) 會自動呼叫 onProgressUpdate() 方法,並將 i 參數代入,在 doInBackground() 中最後 return 的值,亦會自動代入 onPostExecute() 的參數並呼叫。
臉書留言
一般留言