一套完善的Android異步任務類
今天向大家介紹一個很有用的異步任務類處理類,分別包含了AsyncTask各個環(huán)節(jié)中的異常處理、大量并發(fā)執(zhí)行而不發(fā)生異常、字符串數(shù)據緩存等功能。并且感謝@馬天宇(http://litesuits.com/)給我的思路與指點。
研究過Android系統(tǒng)源碼的同學會發(fā)現(xiàn):AsyncTask在android2.3的時候線程池是一個核心數(shù)為5線程,隊列可容納10線程,***執(zhí)行128個任務,這存在一個問題,當你真的有138個并發(fā)時,即使手機沒被你撐爆,那么超出這個指標應用絕對crash掉。 后來升級到3.0,為了避免并發(fā)帶來的一些列問題,AsyncTask竟然成為序列執(zhí)行器了,也就是你即使你同時execute N個AsyncTask,它也是挨個排隊執(zhí)行的。 這一點請同學們一定注意,AsyncTask在3.0以后,是異步的沒錯,但不是并發(fā)的。關于這一點的改進辦法,我之前寫過一篇《Thread并發(fā)請求封裝——深入理解AsyncTask類》沒有看過的同學可以看這里,本文是在這個基礎上對AsyncTask做進一步的優(yōu)化。
根據Android4.0源碼我們可以看到,在AsyncTask中默認有兩個執(zhí)行器,ThreadPoolExecutor和SerialExecutor,分別表示并行執(zhí)行器和串行執(zhí)行器。但是默認的并行執(zhí)行器并不能執(zhí)行大于128個任務的處理,所以我們在此定義一個根據lru調度策略的并行執(zhí)行器。源碼可以看這里。
- /**
- * 用于替換掉原生的mThreadPoolExecutor,可以大大改善Android自帶異步任務框架的處理能力和速度。
- * 默認使用LIFO(后進先出)策略來調度線程,可將***的任務快速執(zhí)行,當然你自己可以換為FIFO調度策略。
- * 這有助于用戶當前任務優(yōu)先完成(比如加載圖片時,很容易做到當前屏幕上的圖片優(yōu)先加載)。
- */
- 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;
- }
- /**
- * 一次同時并發(fā)的數(shù)量,根據處理器數(shù)量調節(jié) <br>
- * cpu count : 1 2 3 4 8 16 32 <br>
- * once(base*2): 1 2 3 4 8 16 32 <br>
- * 一個時間段內最多并發(fā)線程個數(shù): 雙核手機:2 四核手機:4 ... 計算公式如下:
- */
- private static int serialOneTime;
- /**
- * 并發(fā)***數(shù)量,當投入的任務過多大于此值時,根據Lru規(guī)則,將最老的任務移除(將得不到執(zhí)行) <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) {
- // 小于單次并發(fā)量直接運行
- mThreadPoolExecutor.execute(r);
- } else {
- // 如果大于并發(fā)上限,那么移除最老的任務
- 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的并發(fā)執(zhí)行優(yōu)化,接下來我們看對異常捕獲的改進。
真正說起來,這并不算是什么功能上的改進,僅僅是一種開發(fā)上的技巧。代碼過長,我刪去了一些,僅留下重要部分。
- /**
- * 安全異步任務,可以捕獲任意異常,并反饋給給開發(fā)者。<br>
- * 從執(zhí)行前,執(zhí)行中,執(zhí)行后,乃至更新時的異常都捕獲。<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... 但就是這一個小優(yōu)化,不僅可以使代碼整齊(我覺得try...catch太多真的很影響代碼美觀),而且在最終都可以由一個onPostExecuteSafely(xxx)來整合處理,使得結構更加緊湊。
讓AsyncTask附帶數(shù)據緩存功能
我們在做APP開發(fā)的時候,網絡訪問都會加上緩存處理,其中的原因我想就不必講了。那么如果讓AsyncTask自身就附帶網絡JSON緩存,豈不是更好?其實實現(xiàn)原理很簡單,就是將平時我們寫在外面的緩存方法放到AsyncTask內部去實現(xiàn),注釋已經講解的很清楚了,這里就不再講了
- /**
- * 本類主要用于獲取網絡數(shù)據,并將結果緩存至文件,文件名為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));
- }
- }
- }
- }
- /**
- * 做聯(lián)網操作,本方法運行在線程中
- */
- 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) { // 若緩存無效,聯(lián)網下載
- res = doConnectNetwork(params);
- if (res == null)
- res = getResultFromCache();
- else
- saveCache(res);
- } else { // 緩存有效,使用緩存
- res = getResultFromCache();
- if (res == null) { // 若緩存數(shù)據意外丟失,重新下載
- 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;
- }
- /**
- * 保存數(shù)據,并返回是否成功
- */
- 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 將要緩存的數(shù)據
- */
- private void saveCache(Result res) {
- if (res != null) {
- saveResultToCache(res);
- cacheMap.put(cacheName, System.currentTimeMillis());
- }
- }
- }