浅谈Android中的任务封装

1 概述

1.1 定义

  • 一次UI更新
  • 一次数据库中读写操作
  • 上传或下载一张图片
  • 从网络接口获取数据 等等

抽象而言,任何代码块执行的业务逻辑都可称之为一个任务。最常见的是封装在RunableCallableThread子类的run方法中的业务逻辑。

1.2 任务在哪里执行

  • 当前线程直接方法调用
  • 新建子线程执行
  • 提交到线程池执行
  • 放在handler队列依次调用

即将一个任务分派(委托)到一个线程中执行(也可为当前线程)

1.3 理想特性

设想一下,我们最希望任务具有什么特性

(1)可取消性

Android开发很多是基于组件生命周期回调的,如Activity,Fragment,Service等都有提供了一系列on***回调方法。如当前界面关闭,相关的任务都需取消。如:

  • 取消跟界面相关的所有网络请求
  • handler待处理的Message和Callback清空

如不及时取消,有可能出什么状况:

  • 浪费流量和系统资源
  • 若网络请求响应较慢,导致Activity不能及时销毁,甚者内存泄漏
  • 执行回调,则容易出现BadTokenExceptionNullPointException异常(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 <V> Future<V> runInPoolThread(final Callable<V> task) {
        return executor.submit(new Callable<V>() {
            @Override
            public V call() {
                try {
                    return task.call();
                } catch (Exception e) {
                    e.printStackTrace();
                    return null;
                }
            }
        });
    }
}

进行取消操作

Future<String> sayHiFuture = TaskExecutor.runInPoolThread(new Callback<String>(){
    public String call(){
        return "hello word!";
    }
});

sayHiFuture.cancle(true); //进行取消 cancel(boolean mayInterruptIfRunning)

一个任务的状态有等待执行中已完成已取消,其中已完成包括正常完成的和取消完成的。线程池维持一个任务队列,按一定的策略进行调度。调用cancle后,若在对应任务还在等待中,则顺利取消任务,如是在执行中,则更加参数mayInterruptIfRunning决定是否进行发起中断请求。那重点来了,如何处理该中断请求,要看具体的任务,或许被任务直接忽略,继续执行完任务。

结合上面的小结,不难发现:中断是一种协商机制。当前线程发起中断请求,目标线程需要在特定条件(阻塞)或主动去判断中断状态。是否取消,最终决定权在目标线程。

题外话:不要直接new一个线程执行,最佳方案是由线程池来调度

2.2 同异步变化

(1) 同步变异步

基本思路是,让任务在非当前线程执行,看需要是否有必要将执行结果进行回调。如

//同步的
UserInfo getUserInfo(long uid){
    .... 
}
//改为异步
Future<UserInfo> getUserInfo(final long uid, final Callback<UserInfo> callback){
    return TaskExecutor.runInPoolThread(new Callback<UserInfo>(){
        public String call(){
            UserInfo userInfo = getUserInfo(uid);
            if(callback!=null){
                callback.onCallback(userInfo);
            }
            return userInfo;
        }
    });
}
(2) 异步变同步

基本思路是,调用异步的线程一直等待(或规定时间),直到有有回调结果。如下面典型的例子:

//异步的
void getUserInfo (long uid, Callback<UserInfo> callback) {
}

//改为同步的
UserInfo getUserInfo (long uid) {
    final CountDownLatch latch = new CountDownLatch(1);
    final AtomicReference<UserInfo> resultRef = new AtomicReference<UserInfo>();
    getUserInfo (uid, new Callback<UserInfo>() {
        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<UserInfo>() {
        public void onCallback(UserInfo userInfo){
            // 处理你的业务逻辑
        }
    }); 
    
    // 同步使用方式
    Future<UserInfo> 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就是解决任务串的利器。这里不细说,今后会分享多包协议设计就是这种思想的实践。

总结

希望我们封装的任务是可取消的,可组合的,同时也兼备同异步。或许很难具备所有特性,但是可取消性是最基本要求。可取消可取消可取消。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容