Android多執行緒方式
1、前言
在Android開發中經常會使用到多執行緒,這裡主要是總結Android開發中常見的多執行緒實現方式,以及這些多執行緒實現方式的一些特點
多執行緒實現方式主要有:
- 實現Thread的run()方法或者實現Runable介面
- HandlerThread
- AsyncTask
- LoaderManager
2、Thread方式
一般使用非同步操作最常見的一種方式,我們可以繼承Thread,並重寫run()方法,如下所示:
Thread syncTask = new Thread() {
@Override
public void run() {
// 執行耗時操作
}
};
syncTask.start();
還有另外一種啟動執行緒的方式,即在建立Thread物件時,傳入一個實現了Runable介面的物件,如下所示:
Thread syncTask = new Thread(new Runnable() {
@Override
public void run() {
// 執行耗時操作
}
});
syncTask.start();
Thread類中有幾個方法的作用有些模糊,這裡給出說明:
- interrupt( ):
我們一般會使用該方法中斷執行緒的執行,但該方法並不會中斷執行緒,它的作用只是設定一箇中斷標誌位,我們還得在run( )方法中判斷這個標誌位,並決定是否繼續執行,通過這樣的方式來達到中斷的效果。 但該方法根據執行緒狀態的不同,會有不同的結果。結果如下:
- 當執行緒由於呼叫wait( )、join( )、sleep( )而阻塞時,中斷標誌位將會被清空,並接收到InterruptedException異常。
- 當執行緒由於正在進行InterruptibleChannel型別的I/O操作而阻塞時,中斷標誌位將會置位,並接收到ClosedByInterruptException異常(I/O流也會自動關閉)
- 當執行緒由於進行Selector操作而阻塞時,中斷標誌位將會置位,但不會接收到異常
interrupted( )和isInterrupted( )的區別:
兩個方法都是判斷當前執行緒的中斷標誌位是否被置位,但呼叫interrupted( )方法後,中斷標誌位將會重置,而isInterrupted()不會被重置。
- join( )和sleep( )的區別:兩個方法都會讓執行緒暫停執行
join()方法是讓出執行資源(如:CPU時間片),使得其它執行緒可以獲得執行的資源。所以呼叫join()方法會使進入阻塞狀態,該執行緒被喚醒後會進入runable狀態,等待下一個時間片的到來才能再次執行。
sleep( )不會讓出資源,只是處於睡眠狀態(類似只執行空操作)。呼叫sleep()方法會使進入等待狀態,當等待時間到後,如果還在時間片內,則直接進入執行狀態,否則進入runable狀態,等待下個時間片。
3、HandlerThread
有些需求需要子執行緒不斷的從一個訊息佇列中取出訊息,並進行處理,處理完畢以後繼續取出下一個處理。對於這個需求我們可以使用第一種方式,實現一個Thread物件,並建立一個訊息佇列,在Thread物件的run方法中不斷的從訊息佇列中取出訊息進行處理。多以該執行緒的這些特點有點像一個Looper執行緒,我們可複用Handler機制提供的訊息佇列MessageQueue,而無需自己重新建立。
HandlerThread的內部實現機制很簡單,在建立新的執行緒後,使該執行緒成為一個Looper執行緒,讓該執行緒不斷的從MessageQueue取出訊息並處理。我們看一下HandlerThread的實現:
public class HandlerThread extends Thread {
int mPriority;
Looper mLooper;
/**
* Constructs a HandlerThread.
*/
public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}
@Override
public void run() {
// 要想讓某個執行緒成為Looper執行緒,先呼叫Looper.prepare()為該執行緒建立一個Looper物件,並初始化MessageQueue物件
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
// 呼叫Looper.loop(),讓該執行緒的Looper例項迴圈從MessageQueue中取出Message進行處理
Looper.loop();
}
}
4、AsyncTask
這是我們最經常使用的一種非同步方式,在前面的兩種多執行緒方式中,如果在子執行緒中進行了耗時的處理操作(如:網路請求、讀寫資料庫等),當操作完畢後,我們需要更新UI上的顯示狀態,但在Android開發中我們是不能在子執行緒中更新UI介面的,所以還得在子執行緒中傳送一個通知到主執行緒,讓主執行緒去更新UI。這樣的操作流程有些複雜,且都是重複性的工作。所以Android sdk中為我們抽象出AsyncTask這個類。
public class CustomAsyncTask extends AsyncTask<String, Integer, String> {
@Override
protected void onPreExecute() {
// 在開始執行非同步操作前回調,該方法在主執行緒中執行
}
@Override
protected String doInBackground(String... strings) {
// 在該方法中進行非同步操作,引數strings是在啟動非同步任務時execute(...)傳遞進來的
// 該非同步任務放回的結果型別為String
return null;
}
@Override
protected void onProgressUpdate(Integer... values) {
// 該方法使用者通知使用者doInBackground()方法的處理進度,在主執行緒中被回撥,所以可在該方法中更新UI
// 引數values用於指示處理進度
}
@Override
protected void onPostExecute(String result) {
// 該方法是在非同步操作doInBackground()處理完畢後回撥,引數result是doInBackground()的處理結果
// 該方法在主執行緒中被回撥,可直接更新UI
}
@Override
protected void onCancelled(String result) {
super.onCancelled(result);
// 當呼叫cancel(boolean), 則在doInBackground()完成後回撥該方法
// 注意: 引數result可能為null,
}
}
AsyncTask的內部使用了兩個執行緒池,我們大概看一下AsyncTask的內部實現
// 順序執行任務的執行緒池,注意這個執行緒池是靜態的,每個AsyncTask物件共用這個執行緒池
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
// 我們啟動非同步任務的三個方法,都是向SerialExecutor.execute(runable)傳遞一個runable物件
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
...
exec.execute(mFuture);
...
return this;
}
public static void execute(Runnable runnable) {
sDefaultExecutor.execute(runnable);
}
看一下SerialExecutor的實現
private static class SerialExecutor implements Executor {
// 儲存待執行的非同步任務
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
// 其實並沒有馬上執行,而是新增到佇列mTasks中, 進行一個排隊
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
// 一個任務執行完後,再執行下一個
scheduleNext();
}
}
});
// 當前沒有非同步任務執行時,啟動開始執行
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
// 使用另外一個執行緒池分配執行緒,並執行任務
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
所以在使用AsyncTask執行非同步操作時,會先在SerialExecutor進行一個順序排隊, 後再用ThreadPoolExcutor執行緒池為你分配一個執行緒並執行。而整個應用的AsyncTask任務都在排同一條隊,有可能等待排隊的任務很多,所以一般不會使用AsyncTask執行一些優先順序比較高的非同步任務。
當然我們是可以跳過不需要進行排隊,直接就通過執行緒池分配一個執行緒並執行非同步任務,但需要注意同時執行太多的非同步任務,會影響使用者體驗,我想Google就是為了限制同時建立太多的執行緒才會採用一個排隊機制的
/** @hide */
public static void setDefaultExecutor(Executor exec) {
sDefaultExecutor = exec;
}
該方法是隱藏,但可使用反射,設定一個執行緒池。
5、Loader&LoaderManager
上面三種非同步方式都可以用來載入一些耗時的資料,但有時我們載入資料的過程與Activity、Fragment的生命息息相關的。所以在使用上面說的那幾種非同步方式進行非同步資料載入時,是需要去考慮Activity(Fragment)的生命週期是處於哪個階段的。於是Android在Android 3.0以後引入了LoaderManager,主要用於執行一些耗時的非同步資料載入操作,並根據Activity生命週期對非同步處理進行調整,LoaderManager可以解決的問題包括:
- 載入的資料有變化時,會自動通知我們,而不自己監控資料的變化情況,如:用CursorLoader來載入資料庫資料,當資料庫資料有變化時,可是個展示變化的資料
- 資料的請求處理時機會結合Activity和Fragment的生命週期進行調整,如:若Acivity銷燬了,那就不會再去請求新的資料
使用該方法載入資料涉及到兩個類重要的類,Loader和LoaderManager:
Loader:該類用於資料的載入 ,型別引數D用於指定Loader載入的資料型別
public class Loader<D> {
}
一般我們不直接繼承Loader,而是繼承AsyncTaskLoader,因為Loader的載入工作並不是在非同步執行緒中。而AsyncTaskLoader實現了非同步執行緒,載入流程在子執行緒中執行。注意:對該類的呼叫應該在主執行緒中完成。
LoaderManager:
LoaderManager用於管理與Activity和Fragment關聯的Loader例項,LoaderManager負責根據的Activity的生命週期對Loader的資料載入器進行排程,所以這裡分工明確,Loader負責資料載入邏輯,LoaderManager
負責Loader的排程,開發者只需要自定義自己的Loader,實現資料的載入邏輯,而不再關注資料載入時由於Activity銷燬引發的問題。
注意:其實AsyncTaskLoader內部實現非同步的方式是使用AsyncTask完成的,上面我們說過AsyncTask的內部是有一個排隊機制,但AsyncTaskLoader內部使用AsyncTask進行資料非同步載入時,非同步任務並不進行排隊。而直接又執行緒池分配新執行緒來執行。
6、總結
我們來總結一下非同步處理的方式,以及每種處理方式適合什麼樣的場景
- 直接使用Thread實現方式,這種方式簡單,但不是很優雅。適合數量很少(偶爾一兩次)的非同步任務,但要處理的非同步任務很多的話,使用該方式會導致建立大量的執行緒,這會影響使用者互動。
- HandlerThread,這種方式適合子執行緒有序的執行非同步操作,非同步任務的執行一個接著一個。
- AsyncTask, 通常用於耗時的非同步處理,且時效性要求不是非常高的那種非同步操作。如果時效性要求非常高的操作,不建議使用這個方式,因為AsyncTask的預設實現是有內部排隊機制,且是整個應用的AsyncTask的任務進行排隊,所以不能保證非同步任務能很快的被執行。
- LoaderManager,當請求處理時機需要根據Activity的生命週期進行調整,或需要時刻監測資料的變化,那LoaderManager是很不錯的解決方案。