「Android」Android-Bolts-更簡單的完成執行緒排程和任務管理

尚妝產品技術刊讀發表於2017-11-21

本文來自尚妝Android團隊 路遠
發表於尚妝github部落格,歡迎訂閱!

尤塞恩·聖利奧·博爾特 Usain St Leo Bolt,牙買加短跑運動員,男子100米、男子200米以及男子400米接力賽的世界紀錄保持人,同時是以上三項賽事的連續三屆奧運金牌得主。

使用 Bolts 可以將一個完整的操作拆分成多個子任務,這些子任務可以自由的拆分、組合和替換,每個任務作為整個任務鏈的一環可以執行在指定執行緒中,同時既能從上行任務中獲取任務結果,又可以向下行任務釋出當前任務的結果,而不必考慮執行緒之間的互動。

Bolts-Android Bolts 在 Android 下的實現
Bolts-ObjC Bolts 在 OC 下的實現
Bolts-Swift Bolts 在 Swift 下的實現

前言

一個關於執行緒排程的簡單需求,在子執行緒從網路下載圖片,並返回下載的圖片,在主執行緒使用該圖片更新到 UI,同時返回當前 UI 的狀態 json,在子執行緒將 json 資料儲存到本地檔案,完成後在主執行緒彈出提示,這中間涉及到了 4 次執行緒切換,同時後面的任務需要前面任務完成後的返回值作為引數。

使用 Thread + Handler 實現,執行緒排程很不靈活,程式碼可讀性差,不美觀,擴充套件性差,錯誤處理異常麻煩。

String url = "http://www.baidu.com";
Handler handler = new Handler(Looper.getMainLooper());
new Thread(() -> {
    // 下載
    Bitmap bitmap = downloadBitmap(url);
    handler.post(() -> {
        // 更新 UI
        String json = updateUI(bitmap);
        new Thread(() -> {
            // 向儲存寫入UI狀態
            saveUIState(json);
            // 儲存成功後,提示
            handler.post(() -> toastMsg("save finish."));
        }).start();
    });
}).start();複製程式碼

使用 RxJava 實現,執行緒排程非常靈活,鏈式呼叫,程式碼清晰,擴充套件性好,有統一的異常處理機制,不過 Rx 是一個很強大的庫,如果只用來做執行緒排程的話,Rx 就顯得有點太重了。

Observable.just(URL)
        // 下載
        .map(this::downloadBitmap)
        .subscribeOn(Schedulers.newThread())
        // 更新UI
        .observeOn(AndroidSchedulers.mainThread())
        .map(this::updateUI)
        // 儲存 UI 狀態
        .observeOn(Schedulers.io())
        .map(this::saveUIState)
        // 顯示提示
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(rst -> toastMsg("save to " + rst),
                // handle error
                Throwable::printStackTrace);複製程式碼

使用 bolts 實現,執行緒排程靈活,鏈式呼叫,程式碼清晰,具有良好的擴充套件性,具有統一的異常處理機制,雖然沒有 Rx 那麼豐富的操作符,但是勝在類庫非常非常小,只有 38 KB。

Task
        .forResult(URL)
        // 下載
        .onSuccess(task -> downloadBitmap(task.getResult()), Task.BACKGROUND_EXECUTOR)
        // 更新UI
        .onSuccess(task -> updateUI(task.getResult()), Task.UI_THREAD_EXECUTOR)
        // 儲存UI狀態
        .onSuccess(task -> saveUIState(task.getResult()), Task.BACKGROUND_EXECUTOR)
        // 提示
        .onSuccess(task -> toastMsg("save to " + task.getResult()), Task.UI_THREAD_EXECUT
        // handle error
        .continueWith(task -> {
            if (task.isFaulted()) {
                task.getError().printStackTrace();
                return false;
            }
            return true;
        });複製程式碼

執行緒排程器

共有 4 種型別執行執行緒,將任務分發到指定執行緒執行,分別是

  1. backgroud - 後臺執行緒池,可以併發執行任務。
  2. scheduled - 單執行緒池,只有一個執行緒,主要用來執行 delay 操作。
  3. immediate - 即時執行緒,如果執行緒呼叫棧小於 15,則在當前執行緒執行,否則代理給 background
  4. uiThread - 針對 Android 設計,使用 Handler 傳送到主執行緒執行。

backgroud

主要用來在後臺併發執行多工

public static final ExecutorService BACKGROUND_EXECUTOR = BoltsExecutors.background();複製程式碼

Android 平臺下根據 CPU 核數建立執行緒池,其他情況下,建立快取執行緒池。

background = !isAndroidRuntime()
    ? java.util.concurrent.Executors.newCachedThreadPool()
    : AndroidExecutors.newCachedThreadPool();複製程式碼

scheduled

主要用於任務之間做 delay 操作,並不實際執行任務。

scheduled = Executors.newSingleThreadScheduledExecutor();複製程式碼

immediate

主要用來簡化那些不指定執行執行緒的方法,預設在當前執行緒去執行任務,使用 ThreadLocal 儲存每個執行緒呼叫棧的深度,如果深度不超過 15,則在當前執行緒執行,否則代理給 backgroud 執行。

private static final Executor IMMEDIATE_EXECUTOR = BoltsExecutors.immediate();

// 關鍵方法
@Override
public void execute(Runnable command) {
  int depth = incrementDepth();
  try {
    if (depth <= MAX_DEPTH) {
      command.run();
    } else {
      BoltsExecutors.background().execute(command)
    }
  } finally {
    decrementDepth();
  }
}複製程式碼

uiThread

Android 專門設計,在主執行緒執行任務。

public static final Executor UI_THREAD_EXECUTOR = AndroidExecutors.uiThread();複製程式碼
private static class UIThreadExecutor implements Executor {
  @Override
  public void execute(Runnable command) {
    new Handler(Looper.getMainLooper()).post(command);
  }
}複製程式碼

核心類

Task,最核心的類,每個子任務都是一個 Task,它們負責自己需要執行的任務。每個 Task 具有 3 種狀態 ResultErrorCancel,分別代表成功、異常和取消。

Continuation,是一個介面,它就像連結子任務每一環的鎖釦,把一個個獨立的任務連結在一起。

通過 Task - Continuation - Task - Continuation ... 的形式組成完整的任務鏈,順序在各自執行緒執行。

建立 Task

根據 Task 的 3 種狀態,建立簡單的 Task,會複用已有的任務物件

public static <TResult> Task<TResult> forResult(TResult value)

public static <TResult> Task<TResult> forError(Exception error)

public static <TResult> Task<TResult> cancelled()複製程式碼

使用 delay 方法,延時執行並建立 Task

public static Task<Void> delay(long delay)

public static Task<Void> delay(long delay, CancellationToken cancellationToken)複製程式碼

使用 whenAny 方法,執行多個任務,當任意任務返回結果時,儲存這個結果

public static <TResult> Task<Task<TResult>> whenAnyResult(Collection<? extends Task<TResult>> tasks)

public static Task<Task<?>> whenAny(Collection<? extends Task<?>> tasks)複製程式碼

使用 whenAll 方法,執行多個任務,當全部任務執行完後,返回結果

public static Task<Void> whenAll(Collection<? extends Task<?>> tasks) 

public static <TResult> Task<List<TResult>> whenAllResult(final Collection<? extends Task<TResult>> tasks)複製程式碼

使用 call 方法,執行一個任務,同時建立 Task

public static <TResult> Task<TResult> call(final Callable<TResult> callable, Executor executor,
      final CancellationToken ct)複製程式碼

連結子任務

使用 continueWith 方法,連結一個子任務,如果前行任務已經執行完成,則立即執行當前任務,否則加入佇列中,等待。

public <TContinuationResult> Task<TContinuationResult> continueWith(
      final Continuation<TResult, TContinuationResult> continuation, final Executor executor,
      final CancellationToken ct)複製程式碼

使用 continueWithTask 方法,在當前任務之後連結另一個任務鏈,這種做法是為了滿足那種將部分任務組合在一起分離出去,作為公共任務的場景,他接受將另外一個完全獨立的任務鏈,追加在當前執行的任務後面。

public <TContinuationResult> Task<TContinuationResult> continueWithTask(
      final Continuation<TResult, Task<TContinuationResult>> continuation, final Executor executor,
      final CancellationToken ct)複製程式碼

使用 continueWhile 方法連結子任務,與 continueWith 區別在於,他有一個 predicate 表示式,只有當表示式成立時,才會追加子任務,這樣做是在執行任務前可以做一個攔截操作,也是為了不破環鏈式呼叫的整體風格。

public Task<Void> continueWhile(final Callable<Boolean> predicate,
      final Continuation<Void, Task<Void>> continuation, final Executor executor,
      final CancellationToken ct)複製程式碼

使用 onSuccessonSuccessTask 連結單個任務個任務鏈,區別於 continueWith 在於,onSuccess 方法,前行任務如果失敗了,後行的任務也會直接失敗,不會再執行,但是 continueWith 的各個子任務之間沒有關聯,就算前行任務失敗,後行任務也會執行。

public <TContinuationResult> Task<TContinuationResult> onSuccess(
      final Continuation<TResult, TContinuationResult> continuation, Executor executor,
      final CancellationToken ct)複製程式碼

取消任務

Task 沒有 cancel 方法,而是使用了 CancellationToken 作為標記,任務執行之前會檢查這個標記,如果標記為退出,則會直接退出任務。

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
CancellationToken token =   cancellationTokenSource.getToken();
Task.call((Callable<String>) () -> null,
        Task.BACKGROUND_EXECUTOR,
        token);
// 取消任務
cancellationTokenSource.cancel();複製程式碼

異常的處理

關於異常的處理,整個機制下來,每個任務作為一個獨立的單位,異常會被統一捕捉,因此不必針對任務中的方法進行單獨的處理。

如果使用了 continueWith 連結任務,那麼當前任務的的異常資訊,將會儲存在當前 Task 中在下行任務中進行處理,下行任務也可以不處理這個異常,直接執行任務,那麼這個異常就到這裡停止了,不會再向下傳遞,也就是說,只有下行任務才知道當前任務的結果,不管是成功還是異常。

當然了,如果任務之間有關聯,由於上行任務的異常極大可能造成當前任務的異常,那麼當前任務異常的資訊,又會向下傳遞,但是上行任務的異常就到這裡為止了。

如果使用 onSuccess 之類的方法,如果上行任務異常了,那麼下行任務根本不會執行,而是直接將異常往下面傳遞,直到被處理掉。

任務的分離和組合

我們可以將一個完整的操作細分成多個任務,每個任務都遵循單一職責的原則而儘量簡單,這樣可以在任務之間再穿插新的任務,或者將部分任務分離出來組合到一起等。

擴充套件性

我們可以在兩個細分的任務之間新增一個新的操作,而不影響上行和下行任務,如我們給文章開頭的需求中更新 UI 之前,將 Bitmap 先儲存到本地。

Task
        .forResult(URL)
        // 下載
        .onSuccess(task -> downloadBitmap(task.getResult()), Task.BACKGROUND_EXECUTOR)
        // 儲存在本地
        .onSuccess(task -> saveBitmapToFile(task.getResult()),Task.BACKGROUND_EXECUTOR)
        // 更新UI
        .onSuccess(task -> updateUI(task.getResult()), Task.UI_THREAD_EXECUTOR)
        ...複製程式碼

複用性

對一些公共的操作,可以單獨分離成新的任務,當需要做類似操作時,即可複用這部份功能,如可以將下載圖片並更新 UI儲存狀態並彈出提示 兩塊功能分離出來,作為公共的任務。

// 下載圖片->更新UI
public Continuation<String, Task<String>> downloadImageAndUpdateUI() {
    return task ->
            Task.call(() -> downloadBitmap(task.getResult()), Task.BACKGROUND_EXECUTOR)
                    .continueWith(taskWithBitmap -> updateUI(taskWithBitmap.getResult()), Task.UI_THREAD_EXECUTOR);
}

// 儲存狀態->提示資訊
public Continuation<String, Task<Boolean>> saveStateAndToast() {
    return task ->
            Task.call(() -> saveUIState(task.getResult()), Task.BACKGROUND_EXECUTOR)
                    .continueWith(taskWithPath -> toastMsg("save to " + taskWithPath.getResult()));
}複製程式碼

使用分離的任務

Task
        .forResult(URL)
        .continueWithTask(downloadImageAndUpdateUI())
        .continueWithTask(saveStateAndToast())
        ...複製程式碼

總結

Task 中有一個 continuations 是當前任務後面追加的任務列表,噹噹前任務成功、異常或者取消時,會去執行列表中的後續任務。

通常情況下,我們使用鏈式呼叫構建任務鏈,結果就是一條沒有分支的任務鏈。

新增任務時 :每次新增一個 Continuation,就會生成一個 Task,加到上行任務的 continuations 列表中,等待執行,同時返回當前的 Task,以便後面的任務可以連結到當前任務後面。

執行任務時 :當前任務執行完之後,結果可能有 3 種,都會被儲存到當前的 Task 中,然後檢查 continuations 列表中的後續任務,而當前的 Task 就會作為引數,傳遞到後續連結的任務中,來讓後面的任務得知上行任務的結果。

相關文章