深圳幻海软件技术有限公司 欢迎您!

一套完善的Android异步任务类

2023-03-01

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

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

研究过Android系统源码的同学会发现:AsyncTask在android2.3的时候线程池是一个核心数为5线程,队列可容纳10线程,***执行128个任务,这存在一个问题,当你真的有138个并发时,即使手机没被你撑爆,那么超出这个指标应用绝对crash掉。 后来升级到3.0,为了避免并发带来的一些列问题,AsyncTask竟然成为序列执行器了,也就是你即使你同时execute N个AsyncTask,它也是挨个排队执行的。 这一点请同学们一定注意,AsyncTask在3.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); 
           } 
       } 
   } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.

以上便是对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); 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.

其实从代码就可以看出,仅仅是对原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()); 
        } 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.