一套完善的Android異步任務類

歡迎各位加入我的Android開發羣[257053751]

今天向大家介紹一個很有用的異步任務類處理類,分別包含了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調度策略的並行執行器。源碼可以看這裏

?
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
    /**
     * 用於替換掉原生的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的併發執行優化,接下來我們看對異常捕獲的改進。

真正說起來,這並不算是什麼功能上的改進,僅僅是一種開發上的技巧。代碼過長,我刪去了一些,僅留下重要部分。

?
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
/**
 * 安全異步任務,可以捕獲任意異常,並反饋給給開發者。<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內部去實現,註釋已經講解的很清楚了,這裏就不再講了

?
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
/**
 * 本類主要用於獲取網絡數據,並將結果緩存至文件,文件名爲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());
        }
    }
}

發佈了58 篇原創文章 · 獲贊 73 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章