1. 程式人生 > >Android非同步任務AsyncTask的使用與原理分析

Android非同步任務AsyncTask的使用與原理分析

在上一篇文章《》中說到,在瞭解了Android快取機制後我準備自己動手寫一個LruCache和DiskLruCache二級快取的輕量級的圖片請求框架,在思考如何搭建這個框架時,糾結於用何種方式去下載圖片,是直接new出一個執行緒呢,還是用看起來稍微高大上檔次一點的AsyncTask非同步任務來處理?思來想去,還是虛榮心作怪,還是用AsyncTask吧,正好這個工具類我之前用的也比較少,對它的原理也不是很清楚,趁這個機會,好好學一下AsyncTask的使用,並分析一下其原始碼實現。待分析完AsyncTask之後,接著完成圖片請求框架的編寫。

1、AsyncTask的使用

    分析AsyncTask原理之前,還是好好學習一下它的具體使用方法。

1.1 AsyncTask簡介

    在Android中,我們更新UI的操作必須要在主執行緒(UI執行緒)中進行,而下載圖片、檔案這種操作必須要在子執行緒中進行,Android為我們提供了Handler機制,實現了子執行緒與主執行緒之間的通訊。通常做法就是先new出一個子執行緒Thread,在子執行緒中完成下載操作後,通過handler傳送一條Message給主執行緒,主執行緒收到訊息後,就可以進行UI的更新工作了,如下:
Handler mHadler = new Handler(){
@Override
public void handleMessage(Message msg) {
super
.handleMessage(msg);
if(msg.what == 1){
Bitmap bitmap = (Bitmap) msg.obj;
//更新UI...
}
}
};
private void download(){
new Thread(new Runnable() {
@Override
public void run() {
// 這裡進行下載操作...獲得了圖片的bitmap
//下載完後才,向主執行緒傳送Message
Message msg = Message.obtain
();
msg.obj = bitmap;
msg.what = 1;//區分哪一個執行緒傳送的訊息
mHadler.sendMessage(msg);
}
}).start();
}
可以看到,每次要進行下載工作,我們就得先創建出Thread,然後在主執行緒中寫好handler,為了對這個過程進行封裝,Android提供了AsyncTask非同步任務,AsyncTask對執行緒和handler進行了封裝,使得我們可以直接在AsyncTask中進行UI的更新操作,就好像是在子執行緒進行UI更新一樣。

1.2 建立AsyncTask子類

AsyncTask是一個抽象類,我們必須寫一個子類繼承它,在子類中完成具體的業務下載操作。為了對各種情況更好的封裝,AsyncTask抽象類指定了三個泛型引數型別如下:
public abstract class AsyncTask<Params, Progress, Result>{ ... }
其中,三個泛型型別引數的含義如下:Params:開始非同步任務執行時傳入的引數型別,即doInBackground()方法中的引數型別;Progress:非同步任務執行過程中,返回下載進度值的型別,即在doInBackground中呼叫publishProgress()時傳入的引數型別;Result:非同步任務執行完成後,返回的結果型別,即doInBackground()方法的返回值型別;有了這三個引數型別之後,也就控制了這個AsyncTask子類各個階段的返回型別,如果有不同業務,我們就需要再另寫一個AsyncTask的子類進行處理。

1.3 AsyncTask的回撥方法

前面我們說過,AsyncTask對執行緒和handler進行了封裝,那它的封裝性體現在哪裡呢?其實,AsyncTask的幾個回撥方法正是這種封裝性的體現,使得我們感覺在子執行緒進行UI更新一樣。一個基本的AsyncTask有如下幾個回撥方法:(1)onPreExecute():在執行後臺下載操作之前呼叫,執行在主執行緒中(2)doInBackground():核心方法,執行後臺下載操作的方法,必須實現的一個方法,執行在子執行緒中(3)onPostExecute()後臺下載操作完成後呼叫,執行在主執行緒中因此,AsyncTask的基本生命週期過程為:onPreExecute() --> doInBackground() --> onPostExecute()其中,onPreExecute() 和onPostExecute()分別在下載操作前和下載操作後呼叫,同時它們是在主執行緒中進行呼叫,因此可以在這兩個方法中進行UI的更新操作,比如,在onPreExecute()方法中,將下載等待動畫顯示出來,在onPostExecute()方法中,將下載等待動畫進行隱藏。如果我們想向用戶展示檔案的下載進度情況,這時,我們可以在doInBackground()下載操作中,呼叫publishProgress(),將當前進度值傳入該方法,而publishProgress()內部會去呼叫AsyncTask的另一個回撥方法:(4)onProgressUpdate():在下載操作doInBackground()中呼叫publishProgress()時的回撥方法,用於更新下載進度,執行在主執行緒中因此,在需要更新進度值時,AsyncTask的基本生命週期過程為:onPreExecute() --> doInBackground() --> publishProgress() --> onProgressUpdate() --> onPostExecute()可以看到,AsyncTask的優秀之處在於幾個回撥方法的設定上,只有donInBackground()是執行在子執行緒的,其他三個回撥方法都是在主執行緒中執行,因此,只要在AsyncTask中,就可以實現檔案的後臺下載、UI的更新操作。好了,明白瞭如何建立一個AsyncTask,以及AsyncTask的幾個回撥方法的呼叫時機,我們就可以來實戰體驗一下。在例子中,我們去下載一張圖片,並通過進度條顯示下載的進度。首先實現一個AsyncTask的具體實現類,進行圖片的下載,如下:
public class MyAsyncTask extends AsyncTask<String, Integer, Bitmap>{
private ProgressBar mPreogressBar;//進度條
private ImageView mImageView;//圖片顯示控制元件
public MyAsyncTask(ProgressBar pb,ImageView iv){
mPreogressBar = pb;
mImageView = iv;
}

@Override
protected void onPreExecute() {
super.onPreExecute();
mPreogressBar.setVisibility(View.VISIBLE);
}

@Override
protected Bitmap doInBackground(String... params) {
String urlParams = params[0];//拿到execute()傳過來的圖片url
Bitmap bitmap = null;
URLConnection conn = null;
InputStream is = null;
try {
URL url = new URL(urlParams);
conn = url.openConnection();
is = conn.getInputStream();
//這裡只是為了演示更新進度的功能,實際的進度值需要在從輸入流中讀取時逐步獲取
for(int i = 0; i < 100; i++){
publishProgress(i);
Thread.sleep(50);//為了看清效果,睡眠一段時間
}
//將獲取到的輸入流轉成Bitmap
BufferedInputStream bis = new BufferedInputStream(is);
bitmap = BitmapFactory.decodeStream(bis);
is.close();
bis.close();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return bitmap;
}

@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
mPreogressBar.setProgress(values[0]);
}

@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
mPreogressBar.setVisibility(View.GONE);
mImageView.setImageBitmap(bitmap);
}
}
上面doInBackground()中獲取進度值時,我們只是為了做一個進度值更新呼叫的演示,實際專案檔案下載中,我們可能會對拿到的輸入流進行處理,比如讀取輸入流將檔案儲存到本地,在讀取輸入流的時候,我們就可以獲取到已經讀取的輸入流大小作為進度值了,如下:
//實際專案中如何獲取檔案大小作為進度值及更新進度值
            int totalSize = conn.getContentLength();//獲取檔案總大小
            int size = 0;//儲存當前下載檔案的大小,作為進度值
            int count = 0;
            byte[] buffer = new byte[1024];
            while((count = is.read(buffer)) != -1){
                size += count;//獲取已下載的檔案大小
                //呼叫publishProgress更新進度,它內部會回撥onProgressUpdate()方法
                publishProgress(size,totalSize);
                Thread.sleep(100);//為了看清效果,睡眠一段時間
            }
在MainActivity中使用:
public class MainActivity extends AppCompatActivity {
private ImageView mImageView;
private ProgressBar mProgressBar;
private static String URL = "http://c.hiphotos.baidu.com/baike/s%3D220/sign=86442af5a6c27d1ea1263cc62bd4adaf/42a98226cffc1e17d8f914604890f603738de919.jpg";
private MyAsyncTask asyncTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image);
mImageView = (ImageView) findViewById(R.id.id_image);
mProgressBar = (ProgressBar) findViewById(R.id.pb);
asyncTask = new MyAsyncTask(mProgressBar, mImageView);
asyncTask.execute(URL);//將圖片url作為引數傳入到doInBackground()中
}
}
佈局檔案如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/id_image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/pb"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="30dp" />
</RelativeLayout>
效果如下:
由於需要聯網,注意在AndroidManifest.xml中加入網路訪問許可權。

1.4 取消下載任務

我們先來看兩個現象。 我們在佈局中加一個按鈕,點選這個按鈕再載入一次圖片,佈局如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/id_image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/pb"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="30dp" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="15dp"
android:id="@+id/id_btn"
android:text="載入圖片"
android:textSize="16sp"
android:layout_alignParentBottom="true"
android:onClick="loadImage"/>
</RelativeLayout>
因此,在MainActivity中,我們就需要加入loadImage方法,如下:
public class MainActivity extends AppCompatActivity {
private ImageView mImageView;
private ProgressBar mProgressBar;
private static String url = "http://c.hiphotos.baidu.com/baike/s%3D220/sign=86442af5a6c27d1ea1263cc62bd4adaf/42a98226cffc1e17d8f914604890f603738de919.jpg";
private MyAsyncTask asyncTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image);
mImageView = (ImageView) findViewById(R.id.id_image);
mProgressBar = (ProgressBar) findViewById(R.id.pb);
asyncTask = new MyAsyncTask(mProgressBar, mImageView);
asyncTask.execute(url);
}

public void loadImage(View v){
asyncTask.execute(url);
}
}
現象一:在loadImage()方法中,我們直接再次通過asyncTask.execute()執行載入。看看此時效果如何:
onCreate中初始載入完一次圖片後,我們點選“載入圖片”按鈕,此時程式直接崩潰了!這是因為,每一個new出的AsyncTask只能執行一次execute(),如果同一個AsyncTask多次執行execute()執行將會報錯現象二:我們來修改loadImage()方法,在該方法中,我們在開啟自身MainActivity,使得多次初始化的時候進行載入,如下:
public void loadImage(View v){
Intent i = new Intent(this,MainActivity.class);
startActivity(i);
}
此時效果如下:
在第一次執行程式進入MainActivity,執行execute但在顯示出圖片之前,立即點選“載入圖片”按鈕,新開啟一個MainActivity,我們發現這個MainActivity的進度條沒有立即展示出進度出來,說明這個MainActivity的AsyncTask沒有立即執行doInBackground(),這是因為AsyncTask內部使用的是執行緒池,相當於裡面有一個執行緒佇列,執行一次execute時會將一個下載任務加入到執行緒佇列,只有前一個任務完成了,下一個下載任務才會開始執行為了達到我們想要的效果,我們自然想到把上一個任務給取消掉。的確,AsyncTask為我們提供了cancel()方法來取消一個任務的執行,但是要注意的是,cancel方法並沒有能力真正去取消一個任務,其實只是設定這個任務的狀態為取消狀態,我們需要在doInBackground()下載中進行檢測,一旦檢測到該任務被使用者取消了,立即停止doInBackground()方法的執行。我們先修改MainActivity,根據不同業務需求,在不同地方進行任務的取消,我們這裡在onPause()中進行任務的取消,在MainActivity方法中加入onPause()方法,如下:
@Override
protected void onPause() {
super.onPause();
if(asyncTask != null && asyncTask.getStatus() == AsyncTask.Status.RUNNING){
//cancel只是將對應的任務標記為取消狀態
asyncTask.cancel(true);
}
}
繼續修改AsyncTask,在這裡面進行任務是否被取消的檢測,這裡我們只簡單修改下doInBackground()和onProgressUpdae()方法,實際專案中開自己的業務邏輯來控制,如下:
@Override
protected Bitmap doInBackground(String... params) {
String urlParams = params[0];//拿到execute()傳過來的圖片url
Bitmap bitmap = null;
URLConnection conn = null;
InputStream is = null;
try {
URL url = new URL(urlParams);
conn = url.openConnection();
is = conn.getInputStream();
for(int i = 0; i < 100; i++){
if(isCancelled()){//通過isCancelled()判斷任務任務是否被取消
break;
}
publishProgress(i);
Thread.sleep(50);//為了看清效果,睡眠一段時間
}
BufferedInputStream bis = new BufferedInputStream(is);
bitmap = BitmapFactory.decodeStream(bis);
is.close();
bis.close();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return bitmap;
}

@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
if(isCancelled()){//通過isCancelled()判斷任務任務是否被取消
return;
}
mPreogressBar.setProgress(values[0]);
}
在doInBackground()的for迴圈更新進度過程中,我們持續不斷的監聽任務十分被取消,一旦取消了,儘快退出doInBackground的執行,現在執行效果如下:
可以看到,現在每次點選“載入圖片”按鈕,新的介面都會立即更新