一套完善的Android异步任务类

jopen 10年前

集数据并发,异常传递,网络缓存于一身,一套完整的异步任务处理类的实现

今天向大家介绍一个很有用的异步任务类处理类,分别包含了AsyncTask各个环节中的异常处理、大量并发执行而不发生异常、字符串数据缓存等功能。并且感谢@马天宇(http://litesuits.com/)给我的思路与指点。

研究过Android系统源码的同学会发现:AsyncTask在android2.3的时候线程池是一个核心数为5线程,队列可容纳10线程,最大执行128个任务,这存在一个问题,当你真的有138个并发时,即使手机没被你撑爆,那么超出这个指标应用绝对crash掉。 后来升级到4.0,为了避免并发带来的一些列问题,AsyncTask竟然成为序列执行器了,也就是你即使你同时execute N个AsyncTask,它也是挨个排队执行的。 这一点请同学们一定注意,AsyncTask在4.0以后,是异步的没错,但不是并发的。关于这一点的改进办法,我之前写过一篇《Thread并发请求封装——深入理解AsyncTask类》没有看过的同学可以看这里,本文是在这个基础上对AsyncTask做进一步的优化。

根据Android4.0源码我们可以看到,在AsyncTask中默认有两个执行器,ThreadPoolExecutor和SerialExecutor,分别表示并行执行器和串行执行器。但是默认的并行执行器并不能执行大于128个任务的处理,所以我们在此定义一个根据lru调度策略的并行执行器。源码可以看这里

    /**       * 用于替换掉原生的mThreadPoolExecutor,可以大大改善Android自带异步任务框架的处理能力和速度。       * 默认使用LIFO(后进先出)策略来调度线程,可将最新的任务快速执行,当然你自己可以换为FIFO调度策略。       * 这有助于用户当前任务优先完成(比如加载图片时,很容易做到当前屏幕上的图片优先加载)。       */      private static class SmartSerialExecutor implements Executor {          /**           * 这里使用{@link ArrayDequeCompat}作为栈比{@link Stack}性能高           */          private ArrayDequeCompat<Runnable> mQueue = new ArrayDequeCompat<Runnable>(                  serialMaxCount);          private ScheduleStrategy mStrategy = ScheduleStrategy.LIFO;            private enum ScheduleStrategy {              LIFO, FIFO;          }            /**           * 一次同时并发的数量,根据处理器数量调节 <br>           * cpu count : 1 2 3 4 8 16 32 <br>           * once(base*2): 1 2 3 4 8 16 32 <br>           * 一个时间段内最多并发线程个数: 双核手机:2 四核手机:4 ... 计算公式如下:           */          private static int serialOneTime;          /**           * 并发最大数量,当投入的任务过多大于此值时,根据Lru规则,将最老的任务移除(将得不到执行) <br>           * cpu count : 1 2 3 4 8 16 32 <br>           * base(cpu+3) : 4 5 6 7 11 19 35 <br>           * max(base*16): 64 80 96 112 176 304 560 <br>           */          private static int serialMaxCount;            private void reSettings(int cpuCount) {              serialOneTime = cpuCount;              serialMaxCount = (cpuCount + 3) * 16;          }          public SmartSerialExecutor() {              reSettings(CPU_COUNT);          }          @Override          public synchronized void execute(final Runnable command) {              Runnable r = new Runnable() {                  @Override                  public void run() {                      command.run();                      next();                  }              };              if ((mThreadPoolExecutor).getActiveCount() < serialOneTime) {                  // 小于单次并发量直接运行                  mThreadPoolExecutor.execute(r);              } else {                  // 如果大于并发上限,那么移除最老的任务                  if (mQueue.size() >= serialMaxCount) {                      mQueue.pollFirst();                  }                  // 新任务放在队尾                  mQueue.offerLast(r);              }          }          public synchronized void next() {              Runnable mActive;              switch (mStrategy) {              case LIFO:                  mActive = mQueue.pollLast();                  break;              case FIFO:                  mActive = mQueue.pollFirst();                  break;              default:                  mActive = mQueue.pollLast();                  break;              }              if (mActive != null) {                  mThreadPoolExecutor.execute(mActive);              }          }      }


以上便是对AsyncTask的并发执行优化,接下来我们看对异常捕获的改进。

真正说起来,这并不算是什么功能上的改进,仅仅是一种开发上的技巧。代码过长,我删去了一些,仅留下重要部分。

/**   * 安全异步任务,可以捕获任意异常,并反馈给给开发者。<br>   * 从执行前,执行中,执行后,乃至更新时的异常都捕获。<br>   */  public abstract class SafeTask<Params, Progress, Result> extends          KJTaskExecutor<Params, Progress, Result> {      private Exception cause;        @Override      protected final void onPreExecute() {          try {              onPreExecuteSafely();          } catch (Exception e) {              exceptionLog(e);          }      }      @Override      protected final Result doInBackground(Params... params) {          try {              return doInBackgroundSafely(params);          } catch (Exception e) {              exceptionLog(e);              cause = e;          }          return null;      }      @Override      protected final void onProgressUpdate(Progress... values) {          try {              onProgressUpdateSafely(values);          } catch (Exception e) {              exceptionLog(e);          }      }      @Override      protected final void onPostExecute(Result result) {          try {              onPostExecuteSafely(result, cause);          } catch (Exception e) {              exceptionLog(e);          }      }      @Override      protected final void onCancelled(Result result) {          onCancelled(result);      }  }

其实从代码就可以看出,仅仅是对原AsyncTask类中各个阶段的代码做了一次try..catch... 但就是这一个小优化,不仅可以使代码整齐(我觉得try...catch太多真的很影响代码美观),而且在最终都可以由一个onPostExecuteSafely(xxx)来整合处理,使得结构更加紧凑。

让AsyncTask附带数据缓存功能

我们在做APP开发的时候,网络访问都会加上缓存处理,其中的原因我想就不必讲了。那么如果让AsyncTask自身就附带网络JSON缓存,岂不是更好?其实实现原理很简单,就是将平时我们写在外面的缓存方法放到AsyncTask内部去实现,注释已经讲解的很清楚了,这里就不再讲了

/**   * 本类主要用于获取网络数据,并将结果缓存至文件,文件名为key,缓存有效时间为value <br>   * <b>注:</b>{@link #CachedTask#Result}需要序列化,否则不能或者不能完整的读取缓存。<br>   */  public abstract class CachedTask<Params, Progress, Result extends Serializable>          extends SafeTask<Params, Progress, Result> {      private String cachePath = "folderName"; // 缓存路径      private String cacheName = "MD5_effectiveTime"; // 缓存文件名格式      private long expiredTime = 0; // 缓存时间      private String key; // 缓存以键值对形式存在      private ConcurrentHashMap<String, Long> cacheMap;        /**       * 构造方法       * @param cachePath  缓存路径       * @param key  存储的key值,若重复将覆盖       * @param cacheTime  缓存有效期,单位:分       */      public CachedTask(String cachePath, String key, long cacheTime) {          if (StringUtils.isEmpty(cachePath)                  || StringUtils.isEmpty(key)) {              throw new RuntimeException("cachePath or key is empty");          } else {              this.cachePath = cachePath;              // 对外url,对内url的md5值(不仅可以防止由于url过长造成文件名错误,还能防止恶意修改缓存内容)              this.key = CipherUtils.md5(key);              // 对外单位:分,对内单位:毫秒              this.expiredTime = TimeUnit.MILLISECONDS.convert(                      cacheTime, TimeUnit.MINUTES);              this.cacheName = this.key + "_" + cacheTime;              initCacheMap();          }      }        private void initCacheMap() {          cacheMap = new ConcurrentHashMap<String, Long>();          File folder = FileUtils.getSaveFolder(cachePath);          for (String name : folder.list()) {              if (!StringUtils.isEmpty(name)) {                  String[] nameFormat = name.split("_");                  // 若满足命名格式则认为是一个合格的cache                  if (nameFormat.length == 2 && (nameFormat[0].length() == 32 || nameFormat[0].length() == 64 || nameFormat[0].length() == 128)) {                      cacheMap.put(nameFormat[0], TimeUnit.MILLISECONDS.convert(StringUtils.toLong(nameFormat[1]), TimeUnit.MINUTES));                  }              }          }      }        /**       * 做联网操作,本方法运行在线程中       */      protected abstract Result doConnectNetwork(Params... params)              throws Exception;        /**       * 做耗时操作       */      @Override      protected final Result doInBackgroundSafely(Params... params)              throws Exception {          Result res = null;          Long time = cacheMap.get(key);          long lastTime = (time == null) ? 0 : time; // 获取缓存有效时间          long currentTime = System.currentTimeMillis(); // 获取当前时间            if (currentTime >= lastTime + expiredTime) { // 若缓存无效,联网下载              res = doConnectNetwork(params);              if (res == null)                   res = getResultFromCache();              else                   saveCache(res);          } else { // 缓存有效,使用缓存              res = getResultFromCache();              if (res == null) { // 若缓存数据意外丢失,重新下载                  res = doConnectNetwork(params);                  saveCache(res);              }          }          return res;      }        private Result getResultFromCache() {          Result res = null;          ObjectInputStream ois = null;          try {              ois = new ObjectInputStream(new FileInputStream(                      FileUtils.getSaveFile(cachePath, key)));              res = (Result) ois.readObject();          } catch (Exception e) {              e.printStackTrace();          } finally {              FileUtils.closeIO(ois);          }          return res;      }        /**       * 保存数据,并返回是否成功       */      private boolean saveResultToCache(Result res) {          boolean saveSuccess = false;          ObjectOutputStream oos = null;          try {              oos = new ObjectOutputStream(new FileOutputStream(                      FileUtils.getSaveFile(cachePath, key)));              oos.writeObject(res);              saveSuccess = true;          } catch (Exception e) {              e.printStackTrace();          } finally {              FileUtils.closeIO(oos);          }          return saveSuccess;      }        /**       * 清空缓存文件(异步)       */      public void cleanCacheFiles() {          cacheMap.clear();          File file = FileUtils.getSaveFolder(cachePath);          final File[] fileList = file.listFiles();          if (fileList != null) {              // 异步删除全部文件              TaskExecutor.start(new Runnable() {                  @Override                  public void run() {                      for (File f : fileList) {                          if (f.isFile()) {                              f.delete();                          }                      }                  }// end run()              });          }// end if      }        /**       * 移除一个缓存       */      public void remove(String key) {          // 对内是url的MD5          String realKey = CipherUtils.md5(key);          for (Map.Entry<String, Long> entry : cacheMap.entrySet()) {              if (entry.getKey().startsWith(realKey)) {                  cacheMap.remove(realKey);                  return;              }          }      }        /**       * 如果缓存是有效的,就保存       * @param res 将要缓存的数据       */      private void saveCache(Result res) {          if (res != null) {              saveResultToCache(res);              cacheMap.put(cacheName, System.currentTimeMillis());          }      }  }

 来自:http://my.oschina.net/kymjs/blog/350565