DustinChu Blog

android AsyncTask(轉)

AsyncTask非同步任務,或稱異步任務,是一個相當常用的類別,是專門用來處理背景任務與UI的類別。
Android 4.0 之後,有明文規定所有的網路行為都不能在主執行緒(Main Thread)執行,
主執行緒又稱UI執行緒(UI Thread),任何有關UI的東西都在主執行緒中執行,若是你的程式佔據主執行緒很久,使用者體驗會非常的差。

AsyncTask

想像一下,按了一個按鈕後,整個App停住五秒會是怎樣的感覺,因此許多耗時的程式建議寫在背景執行,而其中最常見的就是網路的功能。
在此先介紹一下有關ANR(Application Not Responding)的問題,也就是應用程式沒有回應。
你可以試著加入一個Button,在onClick事件裡面做一件很花時間的事情。

1
2
3
4
5
6
7
8
9
button = (Button)findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
for(int i = 0 ; i < 10000000 ; i++){
Log.d("Tag = " , "HI");
}
}
});
  • 跑了一個很大的迴圈,過一下子你的手機會跑出這個警告。

這就是典型的ANR,因為onClick事件是在主執行緒,你佔據主執行緒太久的時間,因此跳出了這個警告,若是按下確定就會關閉你的程式。
要怎麼知道我是不是在主執行緒呢,你可以用以下的程式碼來判斷。
Thread.currentThread().getId()`` 像是你在onClick裡面加入這個Log,Log.d(“onClick = “ , String.valueOf(Thread.currentThread().getId()));` 然後試著印出來,你會發現他會寫1,1就是主執行緒的ID,也是UI Thread。 這樣你大概了解其中一種會產生ANR的問題。 接下來來談談AsyncTask的用法吧,這次舉的例子是從網路下載圖片。 你可能會想,只是下載一張小圖片應該不會花費太久時間,那我寫在主執行緒就好了。 然後就在onClick`裡面寫了以下的程式碼。

1
2
3
4
5
6
7
8
9
10
try {
URL url = new URL("http://i.imgur.com/Uki7N9T.jpg");
//取得圖片的URL
Bitmap bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
//透過BitmapFactory來下載URL的圖片
imageView.setImageBitmap(bitmap);
//設置圖片到ImageView之中
} catch (IOException e) {
e.printStackTrace();
}

透過BitmapFactory來下載圖片,在使用Try Catch來捕捉一些可能的例外,看起來滿正確的,但是實際執行會發現有一個例外。

  • android.os.NetworkOnMainThreadException

意思是,你不能在主執行緒做網路的事情,還是乖乖用AsyncTask吧XD

AsyncTask<Params, Progress, Result>,這是基本的架構,使用泛型來定義參數,
泛型意思是,你可以定義任意的資料型態給他。

  • Params : 參數,你要餵什麼樣的參數給它。

  • Progress : 進度條,進度條的資料型態要用哪種

  • Result : 結果,你希望這個背景任務最後會有什麼樣的結果回傳給你。

此外,AsyncTask會有四個步驟。

  • onPreExecute : 執行前,一些基本設定可以在這邊做。

  • doInBackground : 執行中,在背景做任務。

  • onProgressUpdate : 執行中,當你呼叫publishProgress的時候會到這邊,可以告知使用者進度。

  • onPostExecute : 執行後,最後的結果會在這邊。

拿下載圖片的例子來寫,繼承AsyncTask,並實作四個步驟,

  • 參數說明 : 丟入網址(String),進度條用整數(Integer),拿到圖片(Bitmap)
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
private class GetImage extends AsyncTask<String , Integer , Bitmap>{
@Override
protected void onPreExecute() {
//執行前 設定可以在這邊設定
super.onPreExecute();
}
@Override
protected Bitmap doInBackground(String... params) {
//執行中 在背景做事情
return null;
}
@Override
protected void onProgressUpdate(Integer... values) {
//執行中 可以在這邊告知使用者進度
super.onProgressUpdate(values);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
//執行後 完成背景任務
super.onPostExecute(bitmap);
}
}

這邊你可能不明白String...是什麼意思,這東西的意思是你可以傳單一一個String
或者是一個String陣列都可以。

假如你丟一個String進去,你只要取得第一個元素即可。

String urlStr = params[0];

接著在把剛才下載圖片的程式改寫到doInBackground之中。

1
2
3
4
5
6
7
8
9
10
11
12
protected Bitmap doInBackground(String... params) {
//執行中 在背景做事情
String urlStr = params[0];
try {
URL url = new URL(urlStr);
Bitmap bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
return bitmap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

如此一來,你就會在背景下載圖片,當沒有例外的時候就會回傳。

此時,你可能會想,那我不要等回傳,我直接在doInBackground去改我的圖片就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
protected Bitmap doInBackground(String... params) {
//執行中 在背景做事情
String urlStr = params[0];
try {
URL url = new URL(urlStr);
Bitmap bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
imageView.setImageBitmap(bitmap);
return bitmap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

這時候會跳出一個例外:

  • Only the original thread that created a view hierarchy can touch its views.

意思是,你只能在UI Thread去修改UI,因為你現在是在背景,因此你必須回到UI Thread才能對UI做事情,很勤勞的跑去Google找解,最後你的程式碼可能變成這樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private Bitmap bitmap;
@Override
protected Bitmap doInBackground(String... params) {
//執行中 在背景做事情
String urlStr = params[0];
try {
URL url = new URL(urlStr);
bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
runOnUiThread(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
return bitmap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

事實上,這樣是可以運作沒錯,但是有點太多此一舉。

主執行緒沒辦法用網路 -> 用背景執行 -> 背景執行沒辦法改UI -> 在回去主執行緒。

我們有提到,AsyncTask有四個步驟,我們試著將這四個步驟的執行緒ID都印出來。

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
private class GetImage extends AsyncTask<String , Integer , Bitmap>{
@Override
protected void onPreExecute() {
//執行前 設定可以在這邊設定
super.onPreExecute();
Log.d("Tag onPreExecute" , String.valueOf(Thread.currentThread().getId()));
}
@Override
protected Bitmap doInBackground(String... params) {
//執行中 在背景做事情
Log.d("Tag doInBackground" , String.valueOf(Thread.currentThread().getId()));
publishProgress(100);
return null;
}
@Override
protected void onProgressUpdate(Integer... values) {
//執行中 可以在這邊告知使用者進度
super.onProgressUpdate(values);
Log.d("Tag onProgressUpdate", String.valueOf(Thread.currentThread().getId()));
}
@Override
protected void onPostExecute(Bitmap bitmap) {
//執行後 完成背景任務
super.onPostExecute(bitmap);
Log.d("Tag onPostExecute", String.valueOf(Thread.currentThread().getId()));
}
}

除了背景任務以外都回到主執行緒了,因此你可以在結果的部分在對UI做修改,不用特定在背景那邊在呼叫回主執行緒,這樣太多此一舉了。

如此一來基本的認識應該有了,這裏提供兩個範例。

##範例##

傳入一個網址

1.傳入一個網址,下載網路圖片後顯示在ImageView之中

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
private class GetImage extends AsyncTask<String , Integer , Bitmap>{
@Override
protected void onPreExecute() {
//執行前 設定可以在這邊設定
super.onPreExecute();
}
@Override
protected Bitmap doInBackground(String... params) {
//執行中 在背景做事情
String urlStr = params[0];
try {
URL url = new URL(urlStr);
return BitmapFactory.decodeStream(url.openConnection().getInputStream());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onProgressUpdate(Integer... values) {
//執行中 可以在這邊告知使用者進度
super.onProgressUpdate(values);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
//執行後 完成背景任務
super.onPostExecute(bitmap);
imageView.setImageBitmap(bitmap);
}
}

執行背景程式的方法

1
new GetImage().execute("http://i.imgur.com/Uki7N9T.jpg");

實作進度條的功能

模擬下載三張圖,可是下載完後沒有回傳圖片回去,這個例子是說明怎麼使用進度條。

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
private class GetImage extends AsyncTask<String , Integer , Bitmap>{
private ProgressDialog progressBar;
//進度條元件
@Override
protected void onPreExecute() {
//執行前 設定可以在這邊設定
super.onPreExecute();
progressBar = new ProgressDialog(MainActivity.this);
progressBar.setMessage("Loading...");
progressBar.setCancelable(false);
progressBar.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressBar.show();
//初始化進度條並設定樣式及顯示的資訊。
}
@Override
protected Bitmap doInBackground(String... params) {
//執行中 在背景做事情
int progress = 0;
for (String urlStr : params) {
try {
URL url = new URL(urlStr);
Bitmap bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
} catch (Exception e) {
e.printStackTrace();
}
publishProgress(progress+=33);
//有三張圖 每張圖33%
}
publishProgress(100);
//最後達到100%
return null;
}
@Override
protected void onProgressUpdate(Integer... values) {
//執行中 可以在這邊告知使用者進度
super.onProgressUpdate(values);
progressBar.setProgress(values[0]);
//取得更新的進度
}
@Override
protected void onPostExecute(Bitmap bitmap) {
//執行後 完成背景任務
super.onPostExecute(bitmap);
progressBar.dismiss();
//當完成的時候,把進度條消失
imageView.setImageBitmap(bitmap);
}
}

當你呼叫publishProgress時,丟入一個值,會到onProgressUpdate之中,在這邊更新進度條的進度。

當完成之時,在呼叫dismiss將進度條除去。

看完這篇文章你應該對AsyncTask有一些基本的認識了。

相關文章: