Android Bolts-更简单的完成线程调度和任务管理

移动开发 Android
使用 Bolts 可以将一个完整的操作拆分成多个子任务,这些子任务可以自由的拆分、组合和替换,每个任务作为整个任务链的一环可以运行在指定线程中,同时既能从上行任务中获取任务结果,又可以向下行任务发布当前任务的结果,而不必考虑线程之间的交互。

尤塞恩·圣利奥·博尔特 Usain St Leo Bolt ,牙买加短跑运动员,男子100米、男子200米以及男子400米接力赛的世界纪录保持人,同时是以上三项赛事的连续三届奥运金牌得主。

[[210857]]

使用 Bolts 可以将一个完整的操作拆分成多个子任务,这些子任务可以自由的拆分、组合和替换,每个任务作为整个任务链的一环可以运行在指定线程中,同时既能从上行任务中获取任务结果,又可以向下行任务发布当前任务的结果,而不必考虑线程之间的交互。

Bolts-Android Bolts 在 Android 下的实现 Bolts-ObjC Bolts 在 OC 下的实现 Bolts-Swift Bolts 在 Swift 下的实现

前言

一个关于线程调度的简单需求,在子线程从网络下载图片,并返回下载的图片,在主线程使用该图片更新到 UI,同时返回当前 UI 的状态 json,在子线程将 json 数据保存到本地文件,完成后在主线程弹出提示,这中间涉及到了 4 次线程切换,同时后面的任务需要前面任务完成后的返回值作为参数。

使用 Thread + Handler 实现,线程调度很不灵活,代码可读性差,不美观,扩展性差,错误处理异常麻烦。

 

  1. String url = "http://www.baidu.com"
  2. Handler handler = new Handler(Looper.getMainLooper()); 
  3. new Thread(() -> { 
  4.     // 下载 
  5.     Bitmap bitmap = downloadBitmap(url); 
  6.     handler.post(() -> { 
  7.         // 更新 UI 
  8.         String json = updateUI(bitmap); 
  9.         new Thread(() -> { 
  10.             // 向存储写入UI状态 
  11.             saveUIState(json); 
  12.             // 保存成功后,提示 
  13.             handler.post(() -> toastMsg("save finish.")); 
  14.         }).start(); 
  15.     }); 
  16. }).start(); 

使用 RxJava 实现,线程调度非常灵活,链式调用,代码清晰,扩展性好,有统一的异常处理机制,不过 Rx 是一个很强大的库,如果只用来做线程调度的话, Rx 就显得有点太重了。

 

  1. Observable.just(URL) 
  2.         // 下载 
  3.         .map(this::downloadBitmap) 
  4.         .subscribeOn(Schedulers.newThread()) 
  5.         // 更新UI 
  6.         .observeOn(AndroidSchedulers.mainThread()) 
  7.         .map(this::updateUI) 
  8.         // 存储 UI 状态 
  9.         .observeOn(Schedulers.io()) 
  10.         .map(this::saveUIState) 
  11.         // 显示提示 
  12.         .observeOn(AndroidSchedulers.mainThread()) 
  13.         .subscribe(rst -> toastMsg("save to " + rst), 
  14.                 // handle error 
  15.                 Throwable::printStackTrace); 

使用 bolts 实现,线程调度灵活,链式调用,代码清晰,具有良好的扩展性,具有统一的异常处理机制,虽然没有 Rx 那么丰富的操作符,但是胜在类库非常非常小,只有 38 KB。

 

  1. Task 
  2.         .forResult(URL) 
  3.         // 下载 
  4.         .onSuccess(task -> downloadBitmap(task.getResult()), Task.BACKGROUND_EXECUTOR) 
  5.         // 更新UI 
  6.         .onSuccess(task -> updateUI(task.getResult()), Task.UI_THREAD_EXECUTOR) 
  7.         // 存储UI状态 
  8.         .onSuccess(task -> saveUIState(task.getResult()), Task.BACKGROUND_EXECUTOR) 
  9.         // 提示 
  10.         .onSuccess(task -> toastMsg("save to " + task.getResult()), Task.UI_THREAD_EXECUT 
  11.         // handle error 
  12.         .continueWith(task -> { 
  13.             if (task.isFaulted()) { 
  14.                 task.getError().printStackTrace(); 
  15.                 return false
  16.             } 
  17.             return true
  18.         }); 

线程调度器

共有 4 种类型执行线程,将任务分发到指定线程执行,分别是

  1. backgroud - 后台线程池,可以并发执行任务。
  2. scheduled - 单线程池,只有一个线程,主要用来执行 delay 操作。
  3. immediate - 即时线程,如果线程调用栈小于 15,则在当前线程执行,否则代理给 background 。
  4. uiThread - 针对 Android 设计,使用 Handler 发送到主线程执行。

backgroud

主要用来在后台并发执行多任务

  1. public static final ExecutorService BACKGROUND_EXECUTOR = BoltsExecutors.background(); 

在 Android 平台下根据 CPU 核数创建线程池,其他情况下,创建缓存线程池。

 

  1. background = !isAndroidRuntime() 
  2.     ? java.util.concurrent.Executors.newCachedThreadPool() 
  3.     : AndroidExecutors.newCachedThreadPool(); 

scheduled

主要用于任务之间做 delay 操作,并不实际执行任务。

  1. scheduled = Executors.newSingleThreadScheduledExecutor(); 

immediate

主要用来简化那些不指定运行线程的方法,默认在当前线程去执行任务,使用 ThreadLocal 保存每个线程调用栈的深度,如果深度不超过 15,则在当前线程执行,否则代理给 backgroud 执行。

 

  1. private static final Executor IMMEDIATE_EXECUTOR = BoltsExecutors.immediate(); 
  2.  
  3. // 关键方法 
  4. @Override 
  5. public void execute(Runnable command) { 
  6.   int depth = incrementDepth(); 
  7.   try { 
  8.     if (depth <= MAX_DEPTH) { 
  9.       command.run(); 
  10.     } else { 
  11.       BoltsExecutors.background().execute(command) 
  12.     } 
  13.   } finally { 
  14.     decrementDepth(); 
  15.   } 

uiThread

为 Android 专门设计,在主线程执行任务。

  1. public static final Executor UI_THREAD_EXECUTOR = AndroidExecutors.uiThread(); 

 

  1. private static class UIThreadExecutor implements Executor { 
  2.   @Override 
  3.   public void execute(Runnable command) { 
  4.     new Handler(Looper.getMainLooper()).post(command); 
  5.   } 

核心类

Task ,最核心的类,每个子任务都是一个 Task ,它们负责自己需要执行的任务。每个 Task 具有 3 种状态 Result 、 Error 和 Cancel ,分别代表成功、异常和取消。

Continuation ,是一个接口,它就像链接子任务每一环的锁扣,把一个个独立的任务链接在一起。

通过 Task - Continuation - Task - Continuation ... 的形式组成完整的任务链,顺序在各自线程执行。

创建 Task

根据 Task 的 3 种状态,创建简单的 Task ,会复用已有的任务对象

 

  1. public static <TResult> Task<TResult> forResult(TResult value)  
  2. public static <TResult> Task<TResult> forError(Exception error)  
  3. public static <TResult> Task<TResult> cancelled() 

使用 delay 方法,延时执行并创建 Task

 

  1. public static Task delay(long delay)  
  2. public static Task delay(long delay, CancellationToken cancellationToken) 

使用 whenAny 方法,执行多个任务,当任意任务返回结果时,保存这个结果

 

  1. public static Task> whenAnyResult(Collection> tasks)  
  2. public static Task> whenAny(Collection> tasks) 

使用 whenAll 方法,执行多个任务,当全部任务执行完后,返回结果

 

  1. public static Task whenAll(Collection> tasks)  
  2. public static Task> whenAllResult(final Collection> tasks) 

使用 call 方法,执行一个任务,同时创建 Task

 

  1. public static Task call(final Callable callable, Executor executor,  
  2. final CancellationToken ct) 

链接子任务

使用 continueWith 方法,链接一个子任务,如果前行任务已经执行完成,则立即执行当前任务,否则加入队列中,等待。

 

  1. public <TContinuationResult> Task<TContinuationResult> continueWith( 
  2.       final Continuation<TResult, TContinuationResult> continuation, final Executor executor, 
  3.       final CancellationToken ct) 

使用 continueWithTask 方法,在当前任务之后链接另一个任务链,这种做法是为了满足那种将部分任务组合在一起分离出去,作为公共任务的场景,他接受将另外一个完全独立的任务链,追加在当前执行的任务后面。

 

  1. public <TContinuationResult> Task<TContinuationResult> continueWithTask( 
  2.       final Continuation<TResult, Task<TContinuationResult>> continuation, final Executor executor, 
  3.       final CancellationToken ct) 

使用 continueWhile 方法链接子任务,与 continueWith 区别在于,他有一个 predicate 表达式,只有当表达式成立时,才会追加子任务,这样做是在执行任务前可以做一个拦截操作,也是为了不破环链式调用的整体风格。

 

  1. public Task<Void> continueWhile(final Callable<Boolean> predicate, 
  2.       final Continuation<Void, Task<Void>> continuation, final Executor executor, 
  3.       final CancellationToken ct) 

使用 onSuccess 和 onSuccessTask 链接单个任务个任务链,区别于 continueWith 在于, onSuccess 方法,前行任务如果失败了,后行的任务也会直接失败,不会再执行,但是 continueWith 的各个子任务之间没有关联,就算前行任务失败,后行任务也会执行。

 

  1. public <TContinuationResult> Task<TContinuationResult> onSuccess( 
  2.       final Continuation<TResult, TContinuationResult> continuation, Executor executor, 
  3.       final CancellationToken ct) 

取消任务

 

  1. CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); 
  2. CancellationToken token =   cancellationTokenSource.getToken(); 
  3. Task.call((Callable<String>) () -> null
  4.         Task.BACKGROUND_EXECUTOR, 
  5.         token); 
  6. // 取消任务 
  7. cancellationTokenSource.cancel(); 

异常的处理

关于异常的处理,整个机制下来,每个任务作为一个独立的单位,异常会被统一捕捉,因此不必针对任务中的方法进行单独的处理。

如果使用了 continueWith 链接任务,那么当前任务的的异常信息,将会保存在当前 Task 中在下行任务中进行处理,下行任务也可以不处理这个异常,直接执行任务,那么这个异常就到这里停止了,不会再向下传递,也就是说,只有下行任务才知道当前任务的结果,不管是成功还是异常。

当然了,如果任务之间有关联,由于上行任务的异常极大可能造成当前任务的异常,那么当前任务异常的信息,又会向下传递,但是上行任务的异常就到这里为止了。

如果使用 onSuccess 之类的方法,如果上行任务异常了,那么下行任务根本不会执行,而是直接将异常往下面传递,直到被处理掉。

任务的分离和组合

我们可以将一个完整的操作细分成多个任务,每个任务都遵循单一职责的原则而尽量简单,这样可以在任务之间再穿插新的任务,或者将部分任务分离出来组合到一起等。

扩展性

我们可以在两个细分的任务之间添加一个新的操作,而不影响上行和下行任务,如我们给文章开头的需求中更新 UI 之前,将 Bitmap 先保存到本地。

 

  1. Task 
  2.         .forResult(URL) 
  3.         // 下载 
  4.         .onSuccess(task -> downloadBitmap(task.getResult()), Task.BACKGROUND_EXECUTOR) 
  5.         // 保存在本地 
  6.         .onSuccess(task -> saveBitmapToFile(task.getResult()),Task.BACKGROUND_EXECUTOR) 
  7.         // 更新UI 
  8.         .onSuccess(task -> updateUI(task.getResult()), Task.UI_THREAD_EXECUTOR) 
  9.         ... 

复用性

对一些公共的操作,可以单独分离成新的任务,当需要做类似操作时,即可复用这部份功能,如可以将 下载图片并更新 UI 、 保存状态并弹出提示 两块功能分离出来,作为公共的任务。

 

  1. // 下载图片->更新UI 
  2. public Continuation<String, Task<String>> downloadImageAndUpdateUI() { 
  3.     return task -> 
  4.             Task.call(() -> downloadBitmap(task.getResult()), Task.BACKGROUND_EXECUTOR) 
  5.                     .continueWith(taskWithBitmap -> updateUI(taskWithBitmap.getResult()), Task.UI_THREAD_EXECUTOR); 
  6.  
  7. // 保存状态->提示信息 
  8. public Continuation<String, Task<Boolean>> saveStateAndToast() { 
  9.     return task -> 
  10.             Task.call(() -> saveUIState(task.getResult()), Task.BACKGROUND_EXECUTOR) 
  11.                     .continueWith(taskWithPath -> toastMsg("save to " + taskWithPath.getResult())); 

使用分离的任务

  1. Task 
  2.         .forResult(URL) 
  3.         .continueWithTask(downloadImageAndUpdateUI()) 
  4.         .continueWithTask(saveStateAndToast()) 
  5.         ... 

总结

在 Task 中有一个 continuations 是当前任务后面追加的任务列表,当当前任务成功、异常或者取消时,会去执行列表中的后续任务。

通常情况下,我们使用链式调用构建任务链,结果就是一条没有分支的任务链。

添加任务时:每次添加一个 Continuation ,就会生成一个 Task ,加到上行任务的 continuations 列表中,等待执行,同时返回当前的 Task ,以便后面的任务可以链接到当前任务后面。

执行任务时:当前任务执行完之后,结果可能有 3 种,都会被保存到当前的 Task 中,然后检查 continuations 列表中的后续任务,而当前的 Task 就会作为参数,传递到后续链接的任务中,来让后面的任务得知上行任务的结果。

责任编辑:未丽燕 来源: 尚妆github博客
相关推荐

2021-05-14 11:39:58

SchedulePython工具

2019-11-15 10:16:27

分布式任务框架

2014-03-13 13:44:37

BoltsParse底层库

2020-08-07 09:06:26

CaaS容器技术

2023-07-05 07:48:04

线程池join关闭状态

2023-12-13 13:03:53

任务调度执行XXLJOB

2023-11-16 09:30:27

系统任务

2021-05-31 20:24:16

鸿蒙HarmonyOS应用

2013-12-17 10:15:19

OpenMP任务调度

2023-12-29 09:38:00

Java线程池

2010-11-01 09:17:40

DB2管理服务器

2023-12-26 07:44:00

Spring定时调度

2022-10-17 00:14:55

微服务税mock代理服务

2023-07-31 08:05:30

Spring任务调度

2023-05-08 16:38:46

任务调度分布式任务调度

2023-12-01 08:21:51

开发者Android组件库

2020-10-26 13:12:00

多线程调度随机性

2010-06-23 16:09:40

Linux Bash

2015-02-27 09:39:25

.NETQuqrtz.NET

2023-03-24 15:44:52

Java多线程工具
点赞
收藏

51CTO技术栈公众号