1. 程式人生 > >Android多執行緒方式

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( )方法中判斷這個標誌位,並決定是否繼續執行,通過這樣的方式來達到中斷的效果。 但該方法根據執行緒狀態的不同,會有不同的結果。結果如下:
    1. 當執行緒由於呼叫wait( )、join( )、sleep( )而阻塞時,中斷標誌位將會被清空,並接收到InterruptedException異常。
    2. 當執行緒由於正在進行InterruptibleChannel型別的I/O操作而阻塞時,中斷標誌位將會置位,並接收到ClosedByInterruptException異常(I/O流也會自動關閉)
    3. 當執行緒由於進行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可以解決的問題包括:

  1. 載入的資料有變化時,會自動通知我們,而不自己監控資料的變化情況,如:用CursorLoader來載入資料庫資料,當資料庫資料有變化時,可是個展示變化的資料
  2. 資料的請求處理時機會結合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是很不錯的解決方案。