Android非同步任務類實現方案

張濤OSC發表於2014-12-04

今天向大家介紹一個很有用的非同步任務類處理類,分別包含了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); 
           } 
       } 
   }

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

相關文章