1 概述
1.1 定义
- 一次UI更新
- 一次数据库中读写操作
- 上传或下载一张图片
- 从网络接口获取数据 等等
抽象而言,任何代码块执行的业务逻辑都可称之为一个任务。最常见的是封装在Runable
或Callable
或Thread
子类的run
方法中的业务逻辑。
1.2 任务在哪里执行
- 当前线程直接方法调用
- 新建子线程执行
- 提交到线程池执行
- 放在handler队列依次调用
即将一个任务分派(委托)到一个线程中执行(也可为当前线程)
1.3 理想特性
设想一下,我们最希望任务具有什么特性
(1)可取消性
Android开发很多是基于组件生命周期回调的,如Activity,Fragment,Service等都有提供了一系列on***
回调方法。如当前界面关闭,相关的任务都需取消。如:
- 取消跟界面相关的所有网络请求
- handler待处理的Message和Callback清空
如不及时取消,有可能出什么状况:
- 浪费流量和系统资源
- 若网络请求响应较慢,导致Activity不能及时销毁,甚者内存泄漏
- 执行回调,则容易出现
BadTokenException
或NullPointException
异常(TODO:代码验证)
(2)同异步兼备
若有如下需求:获取用户信息,先从本地数据库读取,有则直接返回,没有则从网络获取。
同步版本
UserInfo getUserInfoFromDb(long uid){
...
}
UserInfo getUserInfoFromNet(long uid){
...
}
UserInfo getUserInfo(long uid){
UserInfo userInfo = getUserInfoFromDb(uid);
if(userInfo!=null){
return userInfo;
}else{
return getUserInfoFromNet(uid);
}
}
异步版
public static interface Callback {
public void onCallback(UserInfo userInfo);
}
void getUserInfoFromDb(long uid, Callback callback) {
...
}
void getUserInfoFromNet(long uid, Callback callback) {
...
}
void getUserInfo(long uid, final Callback callback) {
getUserInfoFromDb(uid, new Callback() {
public void onCallback(UserInfo userInfo) {
if (userInfo != null) {
if (callback != null) {
callback.onCallback(userInfo);
}
} else {
getUserInfoFromNet(uid, new Callback() {
public void onCallback(UserInfo userInfo) {
if (callback != null) {
callback.onCallback(userInfo);
}
}
});
}
}
});
}
很明显组织同步代码块比异步代码块简易许多,可阅读性高,且测试方便,鲁棒性强。而异步代码,不阻塞当前线程,最典型的异步行为是:非UI操作分派到子线程去运行,执行结果可通过handler或其他事件通知机制通知主线程做UI刷新。一个任务若能同时具备同步和异步性,能由调用者选择,则是最佳的。
(3)可组合性
一个UI界面的展示,可能从多个接口获取数据。如个人资料页由基本用户信息和最近动态信息组成。若能将两个数据接口合并请求,且合并响应,上层处理逻辑将很简易。若一类任务都能自由组合,势必快哉。
资料页 = 用户资料接口+个人动态接口(最近一条)
个人动态页 = 个人动态接口(多屏分页)
2 实现
2.1 取消任务
(1) handler
handler简洁易用,维持一个任务(消息)队列,由一个工作线程负责调度。post*(*)
、send*((*)
实现添加任务或发送消息,对应的removeCallbacks(*)
、removeMessages(*)
实现移除任务或消息。使用不当就会导致内存泄漏:
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
由于非静态内部类带有外部类的引用,故mLeakyHandler
和匿名Runnable
都带有SmapleActivity
的引用。任务队列中维持着者两个内部类的引用,并计划在10分钟后执行,故该Activity最快也是在10分钟后才能被GC掉。必须明确一点,若任务延迟执行的时间改为1ms,那么就不太算内存泄漏。或者在onDestory
及时remove该任务,也不会内存泄漏。个人认为的最佳实践是:
- 定义全局的handler,一个UI相关,一个非UI相关
public class TaskExecutor {
private static Handler uiHandler = new Handler(Looper.getMainLooper());
private static Handler workHandler = null;
private static Looper wordLooper = null;
static {
HandlerThread workThread = new HandlerThread("workThread");
workThred.start();
workLooper = workThread.getLooper();
workHandler = new Handler(workLooper);
}
public static Handler uiHandler() {
return uiHandler;
}
public static Handler workHandler() {
return workHandler;
}
}
- 跟当前组件(Activity或Fragment)生命周期相关的任务,及时去除。
(2) 普通子线程
如何取消(终止)一个运行中的线程?调用目标线程的stop()
方法,这种太直接,可能目标线程还没做好停止前准备,丢失数据,目前该方法已经废弃。中断时实现取消的最合适的方式。如下典型的例子:
public class WorkThread extends Thread {
public void run() {
try {
while (!isInterrupted()) {
// 继续工作
}
// 退出工作
} catch (InterruptedException e) {
// 退出工作
}
}
public void cancel() {
interrupt();
}
}
调用cancle()
时,如当线程为阻塞状态(wait,sleep,join或io等待),将立刻抛出InterruptedException
;当线程在非阻塞态,要么在while判断中不符合条件而退出,要么遇到下一个阻塞状态,抛出InterruptedException
。
(3) 线程池
ExecutorService.submit
将返回一个Future
来描述任务。Future
有一个cancel
方法。
public class TaskExecutor {
// .... 加上上面部分实现
private static ExecutorService executor = Executors.newCachedThreadPool();
public static ExecutorService executor() {
return executor;
}
public static Future> runInPoolThread(final Runnable task) {
return executor.submit(new Runnable() {
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public static Future runInPoolThread(final Callable task) {
return executor.submit(new Callable() {
@Override
public V call() {
try {
return task.call();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
});
}
}
进行取消操作
Future sayHiFuture = TaskExecutor.runInPoolThread(new Callback(){
public String call(){
return "hello word!";
}
});
sayHiFuture.cancle(true); //进行取消 cancel(boolean mayInterruptIfRunning)
一个任务的状态有等待
,执行中
,已完成
,已取消
,其中已完成
包括正常完成的和取消完成的。线程池维持一个任务队列,按一定的策略进行调度。调用cancle
后,若在对应任务还在等待中,则顺利取消任务,如是在执行中
,则更加参数mayInterruptIfRunning
决定是否进行发起中断请求。那重点来了,如何处理该中断请求,要看具体的任务,或许被任务直接忽略,继续执行完任务。
结合上面的小结,不难发现:中断是一种协商机制。当前线程发起中断请求,目标线程需要在特定条件(阻塞)或主动去判断中断状态。是否取消,最终决定权在目标线程。
题外话:不要直接new一个线程执行,最佳方案是由线程池来调度
2.2 同异步变化
(1) 同步变异步
基本思路是,让任务在非当前线程执行,看需要是否有必要将执行结果进行回调。如
//同步的
UserInfo getUserInfo(long uid){
....
}
//改为异步
Future getUserInfo(final long uid, final Callback callback){
return TaskExecutor.runInPoolThread(new Callback(){
public String call(){
UserInfo userInfo = getUserInfo(uid);
if(callback!=null){
callback.onCallback(userInfo);
}
return userInfo;
}
});
}
(2) 异步变同步
基本思路是,调用异步的线程一直等待(或规定时间),直到有有回调结果。如下面典型的例子:
//异步的
void getUserInfo (long uid, Callback callback) {
}
//改为同步的
UserInfo getUserInfo (long uid) {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference resultRef = new AtomicReference();
getUserInfo (uid, new Callback() {
public void onCallback(UserInfo userInfo){
try {
resultRef.set(userInfo);
} finally {
latch.countDown();
}
}
});
try {
latch.await(10, TimeUnit.SECONDS); //最多等待10秒
} catch(){
//等待中断
}
return resultRef.get();
}
是否设置等待时间或设置多少决定具体业务需求。个人建议都设置等待时间,否则若回调没有调用,将永远阻塞。上面只是异步代码同步化的一种实现方案,或许你有更好的方案。
(3) 同异步兼备
利用线程池的Future.get()
借用 同步变异步的例子
// ... 同步变异步的代码
// 异步使用方式
getUserInof(1, new Callback() {
public void onCallback(UserInfo userInfo){
// 处理你的业务逻辑
}
});
// 同步使用方式
Future future = getUserInfo(1, null);
UserInfo userInfo = future.get();
future.get()
会一直阻塞,直达关联的任务执行完(可能是被取消的)。我们现在来看OkHttp的封装方式
OkHttpClient client = new OkHttpClient();
//同步版
String run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
Call call = client.newCall(request)
Response response = call.execute();
return response.body().string();
}
// 异步版
void run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
Call call = client.newCall(request)
call.enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
}
@Override
public void onResponse(Response response) throws IOException {
String res = response.body().string()
// 处理业务逻辑
}
});
// call.cancle();//取消任务
}
我们不细究OkHttp的实现细节,重点是借鉴其封装任务的方式。调用某个任务,不直接执行,而是返回一个中间对象Call
(类似Future
),便于对任务进行控制。如同步执行,异步执行,取消,执行状态判断等等。
2.3 组合任务
将大的任务,拆分成粒度小的有依赖关系或独立的子任务。最近比较火的RxJava就是解决任务串的利器。这里不细说,今后会分享多包协议设计就是这种思想的实践。
总结
希望我们封装的任务是可取消的,可组合的,同时也兼备同异步。或许很难具备所有特性,但是可取消性是最基本要求。可取消、可取消、可取消。。。。