对Android客户端编程来说,有个明确的规则是不能在ui线程里面做耗时的操作。这样就要求网络请求、文件读写等等操作都要异步操作。
而异步操作完成后,往往需要再更新ui界面。最直接的想法是回调,只要保证在ui线程里面,更新ui组件不会困难。
但有些情况下,往往需要多层异步操作,这时候代码就很丑了,不管是维护,还是编写都是挑战。
举例
以发布内容举例:
- 先上传所有图片,图片依次上传。
- 图片上传完成后,调用接口发布内容。
这是个很常见的场景,我们先以回调的方式来实现,感受下。
首先,定义图片上传的接口,由于是异步操作,添加回调。
private void uploadImage(Image image, ImageUploadCallback callback) { ... }
第二步,定义多个图片上传的接口,依次上传图片,这时需要用到递归大法。(我不喜欢递归,因为递归要思考下,才能考虑清楚,不管是编写还是维护阅读。)
private void uploadImages(List<Image> images, UploadAllImageCallback callback) { Image poped = images.remove(0); uploadImageRecursive(images, poped, callback); } private void uploadImageRecursive(final List<Image> left, Image image, final UploadAllImageCallback callback) { uploadImage(image, new ImageUploadCallback() { public void onFinished(Exception e, String url) { if (e != null) { if (callback != null) callback.onFinished(e/*failed*/); return; } if (left.isEmpty()) { if (callback != null) callback.onFinished(null/*sucess*/); return; } Image poped = left.remove(0); uploadImageRecursive(left, poped, callback); } }) }
第三步,定义发布内容接口。
private void httpPublishPost(Post post, HttpPublishPostCallback callback) { ... }
第四步,整合发布图片和发布内容。
public void publishPost(Post post, PublishPostCallback callback) { uploadImages(post.images, new UploadAllImageCallback() { public void onFinished(Exception e) { if (e != null) { if (callback != null) callback.onFinished(e); return; } httpPublishPost(post, new HttpPublishPostCallback() { public void onFinished(Exception e) { if (callback != null) { callback.onFinished(e); } } }) } }) }
整套代码至少以下几个方面不利于代码维护:
- 图片发布用到递归,代码不够清晰,编写和维护都不方便
- 最后的整合很操蛋,用到了多层回调。
- java不比js,要定义一堆的回调传递,很痛苦。
Bolts
Javascript 有Promise处理异步操作,程序逻辑扁平化,非常有利于扩展和组合。
对于Android也有类似的解决方案,就是Bolts-Android
iOS也有相应的版本:Bolts-iOS
Bolts 是Parse 开源的框架,Parse 前端时间被Facebook收购,是个云平台服务提供商。
Bolts 提供了一种对任务的封装,将所有的操作抽象为Task, 只包含两种结果,出错 & 成功。并提供了一系列接口,方便开发者使用。
我们仍然以发布内容为例,说明Bolts的使用方法和好处。
首先,定义单个图片上传接口。
private Task<Void> uploadImage(Image image) { return Task.callInBackground(new Callable<Void>() { public Void call() { ... throw new Exception(); //for error //or return null;//for success. } }); }
这里图片上传,返回一个Task对象,而不是传入回调。Task可以用来监听,获取执行完成的事件,以及执行结果。
第二步,定义多个图片上传的接口。牛逼的地方来了哈。
private Task<Void> uploadImages(List<Image> images) { Task<Void> task = Task.forResult(null); for (Image image : images) { task = task.onSuccessTask(new Continuation<Void, Task<Void>>() { public Task<Void> then(Task<Void> task) throws Exception { return uploadImage(image); } }); } return task; }
好处:
- 没有递归没有递归没有递归。
- 错误码直接向下传递,出错后自动中断。
第三步,定义发布内容的接口。
private Task<Void> httpPublishPost(Post post) { ... }
第四步,整合发布图片和发布内容接口。
public Task<Void> publishPost(Post post) { return uploadImages(post.images) .onSuccessTask(new Continuation<Void, Task<Void>>() { public Task<Void> then(Task<Void> task) throws Exception { return httpPublishPost(post); } });}
Bolts 是最近才开始使用,着实对异步处理有很大的帮助。
之前有看过RxJava, 也是一种扁平化的编程方式,对异步处理也很有方便,但包很大,整个编程思想也与原有模式有很大的不同,之前有用到一个小项目上,后来折腾来折腾去,始终没有用顺。
Bolts 很轻量,有作用的类也就Task一个,代码也不多,很喜欢。
数月前,Parse被Facebook收购。最近,它开源了一个面向iOS和Android的底层库集合,统称为Bolts。根据Parse的公告,Bolts是Parse和Facebook共同努力将两家公司各自独立开发的小型底层工具类合并的结果。
Tasks是GitHub上第一个可用的Bolts组件,旨在按照JavaScript Promises模型处理异步操作。
Promises试图解决使用回调函数处理异步操作时通常会出现的若干问题,尤其是这样一个事实:由于回调函数内部异步操作的嵌套,试图组合多个串行或并行异步操作会很快变得难以处理。
为了这个目标,一个Promise代表一项可能已经完成或者可能尚未完成的任务的结果,而它最终可能会变成一个错误。这样,任何异步操作都可以立即在执行结果中返回一个Promise;该Promise可以随时访问,如果异步操作尚未完成,可能阻塞调用者。
不过,一个Promise通常关联两个回调函数,用于在异步任务已经完成或者失败时调用。Promises的特别之处在于回调函数本身封装在Promise之中,所以它们只在将来的某个时间点执行,或者根本不执行,这依赖于原Promise的状况。
多亏这一机制,处理异步操作的序列变得简单易懂,因为Promises可以链到一起来代表异步操作和其回调函数,如上图所示(源自:Promises,Luke Smith。)
Promises的另一项优点在于错误通过Promises链传播的方式:由于Promise知道它是否已经达成,它可以将错误状态沿着Promises链传播,直至找到一个错误处理器,因此,开发人员无需为链上的每个异步操作提供错误处理器。
Promises实现的组件可以用在JavaScript、Scala、Clojure和许多其它语言中。
Parse声称,与Android AsyncTask和iOS NSOperation相比,Tasks有若干优势,其中包括:
- 连续执行数个任务不会像只使用回调函数时那样创建嵌套的“金字塔(pyramid)”代码。
- Tasks是完全可组合的,允许开发人员执行分支、并行和复杂的错误处理。
- 开发人员可以按照执行顺序安排基于任务的代码,而不必将逻辑分解到分散的回调函数中。
github地址 https://github.com/BoltsFramework/Bolts-Android
Android 下的 bolts tasks
本文主要介绍的是在Android 下使用 bolts tasks, bolts tasks 可以很方便的让我们将一些异步task关联起来执行。让这些tasks有顺序的执行(当我们一个task的执行要基于另一个task的时候)。
1. github 地址 Bolts-Android
2. gradle中引用
dependencies {
compile 'com.parse.bolts:bolts-android:1.2.0'
}
3. 具体的使用
(我也是初学者,只是偶尔用过一次。理解的也很简单,我只是列出这个库里面我用到的一些方法,望见谅)
既然是task 肯定要知道他在那个线程中运行的。 Boots tasks 源码中总共有三种形式
/**
* An {@link java.util.concurrent.Executor} that executes tasks in parallel.
*/
public static final ExecutorService BACKGROUND_EXECUTOR = BoltsExecutors.background();
/**
* An {@link java.util.concurrent.Executor} that executes tasks in the current thread unless
* the stack runs too deep, at which point it will delegate to {@link Task#BACKGROUND_EXECUTOR} in
* order to trim the stack.
*/
private static final Executor IMMEDIATE_EXECUTOR = BoltsExecutors.immediate();
/**
* An {@link java.util.concurrent.Executor} that executes tasks on the UI thread.
*/
public static final Executor UI_THREAD_EXECUTOR = AndroidExecutors.uiThread();
BACKGROUND_EXECUTOR 我把他理解成是后台线程。
IMMEDIATE_EXECUTOR 我把他理解成是当前线程,当然如果当前线程路径太深也会是后台线程。
UI_THREAD_EXECUTOR 我把他理解是UI线程(Handler(Looper.getMainLooper()).post())
1). 执行单个的任务
Task.callInBackground(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// 处理一些具体的动作
return true;
}
});
我把他理解成是后台异步处理。
Task.call(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// 处理一些具体的动作
return true;
}
});
我把他理解成是在同一个线程执行。
Task.call(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// 处理一些具体的动作
return true;
}
}, Task.UI_THREAD_EXECUTOR);
UI线程执行。
2). 多个任务顺序执行
Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
// show waiting dialog
showWaitingDialog(R.string.waiting_for_get_wifi_mode);
return null;
}
}).onSuccess(new Continuation<Void, WifiMode>() {
@Override
public WifiMode then(Task<Void> task) throws Exception {
RouterManager routerManager = getRouterManager();
return routerManager.getWifiMode();
}
}, Task.BACKGROUND_EXECUTOR).continueWith(new Continuation<WifiMode, Void>() {
@Override
public Void then(Task<WifiMode> task) throws Exception {
// dismiss dialog
dismissWaitingDialog();
// set ui
if (task.isFaulted()) {
changeUiByWifiMode(mConnectMode);
} else if (task.isCompleted()) {
mConnectMode = task.getResult();
changeUiByWifiMode(mConnectMode);
}
return null;
}
}, Task.UI_THREAD_EXECUTOR);
第一个任务(Task.call的任务) 是在调用者的同一个线程执行。我这里做的动作只是显示waitting dialog。
第二个任务(onSuccess跟上的任务)注意onSuccess的第二个参数是 Task.BACKGROUND_EXECUTOR 表示在后台执行可以做一些耗时的操作,并且会把返回值传到下一个任务。
第三个任务(continueWith跟上的任务)是获取到上一个任务的返回值。在做一些具体的动作。
onSuccess 顾名思义是在第一个任务成功执行的基础上在执行的(第一个任务没有抛异常)。continueWith 顾名思义是在上一个任务执行完的基础上执行的不管有没有抛异常都会执行(当有异常的时候我们会在下文中提到怎么获取到异常)。
3). 多个任务并行执行。
Task.call(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return true;
}
}, Task.UI_THREAD_EXECUTOR).onSuccess(new Continuation<Boolean, List<Task<ReachableIP>>>() {
@Override
public List<Task<ReachableIP>> then(Task<Boolean> task) throws Exception {
List<Task<ReachableIP>> tasks = new ArrayList<Task<ReachableIP>>();
tasks.add(getGatewayAsync());
tasks.add(getRouterWanIpAsync());
Task.whenAll(tasks).waitForCompletion();
return tasks;
}
}, Task.BACKGROUND_EXECUTOR).onSuccess(new Continuation<List<Task<ReachableIP>>, ReachableIP>() {
@Override
public ReachableIP then(Task<List<Task<ReachableIP>>> task) throws Exception {
List<Task<ReachableIP>> tasks = task.getResult();
for (Task<ReachableIP> t : tasks) {
if (t.isCompleted()) {
ReachableIP reachableIP = t.getResult();
}
}
return null;
}
}).onSuccess(new Continuation<ReachableIP, Void>() {
第二个任务中会执行两个任务,等两个任务都执行完了才会往下执行,第三个任务可以获取到上一个中的两个任务的返回值
两个任务的具体声明如下实现部分我去掉了,ReachableIP是自定义的class:
private Task<ReachableIP> getGatewayAsync() {
return Task.callInBackground(new Callable<ReachableIP>() {
@Override
public ReachableIP call() throws Exception {
return null;
}
});
}
private Task<ReachableIP> getRouterWanIpAsync() {
return Task.callInBackground(new Callable<ReachableIP>() {
@Override
public ReachableIP call() throws Exception {
return null;
}
});
}
4). Exception处理
}, Task.BACKGROUND_EXECUTOR).continueWith(new Continuation<BindDeviceInfo, Void>() {
@Override
public Void then(Task<BindDeviceInfo> task) throws Exception {
if (task.isFaulted()) {
Exception exception = task.getError();
if (exception instanceof ResponseError) {
ResponseError e = (ResponseError) exception;
switch (e.errorCode()) {
case ErrorCode.PASSWORD_ERROR:
......
break;
default:
break;
}
}
}
return null;
}
}, Task.UI_THREAD_EXECUTOR);
因为任务在哪里有异常我们是可以预知到的,当有异常的时候,我们catch到再转换为我们自己的异常在thorw出来如上面的ResponseError 是我们自定义的异常,这样我们task.getError出来了可以做不同的处理。
5). delay
<code class="hljs java has-numbering"> <span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">test</span>() { Task.delay(<span class="hljs-number">3000</span>).continueWith(<span class="hljs-keyword">new</span> Continuation<Void, Void>() { <span class="hljs-annotation">@Override</span> <span class="hljs-keyword">public</span> Void <span class="hljs-title">then</span>(Task<Void> task) <span class="hljs-keyword">throws</span> Exception { <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>; } }, Task.BACKGROUND_EXECUTOR); }</code>