在今天這個例子裡,我們用到了之前一篇文章中寫過的一個自訂控制項,如果有同學感興趣的話可以點擊這裡來先研究下這個控制項的實現,為了配合非同步載入的效果,我針對這個控制項做了一點修改,下面會對修改的地方進行解釋。
接下來我們就分別針對ThreadPool和AsyncTask兩種實現方式進行講解,我會順著實現的思路貼出關鍵的代碼,在文章最後會貼出實現效果和源碼下載,感興趣的同學可以下載下來對比來看。
首先來講解ThreadPool(線程池)的實現方式。
我們首先需要來實現一個線程池管理器,這個管理器內部包含一個獨立的輪詢子線程,它的工作是不時的檢查工作隊列,如果隊列中有未執行的任務,就將任務交給線程池來執行。此外,線程池管理器還負責管理線程池和維護任務隊列。具體實現代碼如下:
package com.carrey.asyncloaddemo;import java.util.LinkedList;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import android.util.Log;/** * 線程池管理類 * @author carrey * */public class ThreadPoolManager {private static final String TAG = "ThreadPoolManager";/** 線程池的大小 */private int poolSize;private static final int MIN_POOL_SIZE = 1;private static final int MAX_POOL_SIZE = 10;/** 線程池 */private ExecutorService threadPool;/** 請求隊列 */private LinkedList<ThreadPoolTask> asyncTasks;/** 工作方式 */private int type;public static final int TYPE_FIFO = 0;public static final int TYPE_LIFO = 1;/** 輪詢線程 */private Thread poolThread;/** 輪詢時間 */private static final int SLEEP_TIME = 200;public ThreadPoolManager(int type, int poolSize) {this.type = (type == TYPE_FIFO) ? TYPE_FIFO : TYPE_LIFO;if (poolSize < MIN_POOL_SIZE) poolSize = MIN_POOL_SIZE;if (poolSize > MAX_POOL_SIZE) poolSize = MAX_POOL_SIZE;this.poolSize = poolSize;threadPool = Executors.newFixedThreadPool(this.poolSize);asyncTasks = new LinkedList<ThreadPoolTask>();}/** * 向任務隊列中新增工作 * @param task */public void addAsyncTask(ThreadPoolTask task) {synchronized (asyncTasks) {Log.i(TAG, "add task: " + task.getURL());asyncTasks.addLast(task);}}/** * 從任務隊列中提取任務 * @return */private ThreadPoolTask getAsyncTask() {synchronized (asyncTasks) {if (asyncTasks.size() > 0) {ThreadPoolTask task = (this.type == TYPE_FIFO) ? asyncTasks.removeFirst() : asyncTasks.removeLast();Log.i(TAG, "remove task: " + task.getURL());return task;}}return null;}/** * 開啟線程池輪詢 * @return */public void start() {if (poolThread == null) {poolThread = new Thread(new PoolRunnable());poolThread.start();}}/** * 結束輪詢,關閉線程池 */public void stop() {poolThread.interrupt();poolThread = null;}/** * 實現輪詢的Runnable * @author carrey * */private class PoolRunnable implements Runnable {@Overridepublic void run() {Log.i(TAG, "開始輪詢");try {while (!Thread.currentThread().isInterrupted()) {ThreadPoolTask task = getAsyncTask();if (task == null) {try {Thread.sleep(SLEEP_TIME);} catch (InterruptedException e) {Thread.currentThread().interrupt();}continue;}threadPool.execute(task);}} finally {threadPool.shutdown();}Log.i(TAG, "結束輪詢");}}}
注意在上面的代碼中,我們自訂了任務單元的實現,任務單元是一系列的Runnable對象,最終都將交給線程池來執行,任務單元的實現代碼如下:
ThreadPoolTask.java:
package com.carrey.asyncloaddemo;/** * 任務單元 * @author carrey * */public abstract class ThreadPoolTask implements Runnable {protected String url;public ThreadPoolTask(String url) {this.url = url;}public abstract void run();public String getURL() {return this.url;}}
ThreadPoolTaskBitmap.java:
package com.carrey.asyncloaddemo;import com.carrey.customview.customview.CustomView;import android.graphics.Bitmap;import android.os.Process;import android.util.Log;/** * 圖片載入的任務單元 * @author carrey * */public class ThreadPoolTaskBitmap extends ThreadPoolTask {private static final String TAG = "ThreadPoolTaskBitmap";private CallBack callBack;private CustomView view;private int position;public ThreadPoolTaskBitmap(String url, CallBack callBack, int position, CustomView view) {super(url);this.callBack = callBack;this.position = position;this.view = view;}@Overridepublic void run() {Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);Bitmap bitmap = ImageHelper.loadBitmapFromNet(url);Log.i(TAG, "loaded: " + url);if (callBack != null) {callBack.onReady(url, bitmap, this.position, this.view);}}public interface CallBack {public void onReady(String url, Bitmap bitmap, int position, CustomView view);}}
上面代碼中的回調實現位於MainActivity.java中,在任務單元的run方法中主要做的事情就是從伺服器得到要載入的圖片的Bitmap,然後調用回調,在回調中會將得到的圖片載入到UI介面中。
在載入伺服器圖片的時候,用到了ImageHelper這個工具類,這個類主要的功能就是提供獲得伺服器圖片地址和解析圖片的方法,具體代碼如下:
package com.carrey.asyncloaddemo;import java.io.IOException;import java.io.InputStream;import java.net.URL;import java.net.URLConnection;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.util.Log;/** * 工具類,用於獲得要載入的圖片資源 * @author carrey * */public class ImageHelper {private static final String TAG = "ImageHelper";public static String getImageUrl(String webServerStr, int position) {return "http://" + webServerStr + "/" + (position % 50) + ".jpg";}/** * 獲得網狀圖片Bitmap * @param imageUrl * @return */public static Bitmap loadBitmapFromNet(String imageUrlStr) {Bitmap bitmap = null;URL imageUrl = null;if (imageUrlStr == null || imageUrlStr.length() == 0) {return null;}try {imageUrl = new URL(imageUrlStr);URLConnection conn = imageUrl.openConnection();conn.setDoInput(true);conn.connect();InputStream is = conn.getInputStream();int length = conn.getContentLength();if (length != -1) {byte[] imgData = new byte[length];byte[] temp = new byte[512];int readLen = 0;int destPos = 0;while ((readLen = is.read(temp)) != -1) {System.arraycopy(temp, 0, imgData, destPos, readLen);destPos += readLen;}bitmap = BitmapFactory.decodeByteArray(imgData, 0, imgData.length);}} catch (IOException e) {Log.e(TAG, e.toString());return null;}return bitmap;}}
到這裡準備的工作基本就完成了,接下來的工作就是啟動線程池管理器並向任務隊列添加我們的載入任務了,這部分工作我們放在GridView的Adapter的getView方法中來執行,其中關鍵的代碼如下:
holder.customView.setTitleText("ThreadPool");holder.customView.setSubTitleText("position: " + position);poolManager.start();String imageUrl = ImageHelper.getImageUrl(webServerStr, position);poolManager.addAsyncTask(new ThreadPoolTaskBitmap(imageUrl, MainActivity.this, position, holder.customView));
下面我們來接著講解AsyncTask的實現方式。
相對線程池的實現方式,AsyncTask的實現方式要簡單一些
我們首先來定義好我們的AsyncTask子類,在其中我們將在doInBackground中載入圖片資料,在onPostExecute中來重新整理UI。代碼如下:
package com.carrey.asyncloaddemo;import com.carrey.customview.customview.CustomView;import android.graphics.Bitmap;import android.os.AsyncTask;import android.util.Log;import android.util.Pair;public class AsyncLoadTask extends AsyncTask<Integer, Void, Pair<Integer, Bitmap>> {private static final String TAG = "AsyncLoadTask";/** 要重新整理的view */private CustomView view;public AsyncLoadTask(CustomView view) {this.view = view;}@Overrideprotected void onPreExecute() {super.onPreExecute();}@Overrideprotected Pair<Integer, Bitmap> doInBackground(Integer... params) {int position = params[0];String imageUrl = ImageHelper.getImageUrl(MainActivity.webServerStr, position);Log.i(TAG, "AsyncLoad from NET :" + imageUrl);Bitmap bitmap = ImageHelper.loadBitmapFromNet(imageUrl);return new Pair<Integer, Bitmap>(position, bitmap);}@Overrideprotected void onPostExecute(Pair<Integer, Bitmap> result) {if (result.first == view.position) {view.setImageBitmap(result.second);}}}
在Adapter中調用AsyncTask非同步載入的代碼如下:
holder.customView.setTitleText("AsyncTask");holder.customView.setSubTitleText("position: " + position);new AsyncLoadTask(holder.customView).execute(position);
寫到這裡,關鍵的代碼基本都講完了,我們不妨先來看一下兩種實現方式的效果:
通過對比可以發現,ThreadPool相比AsyncTask,並發能力更強,載入的速度也更快,AsyncTask在載入過程中明顯變現出順序性,載入的速度要慢一些。
下面是調用兩種載入方式的MainActivity的所有代碼:
package com.carrey.asyncloaddemo;import android.app.Activity;import android.content.Context;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.os.Bundle;import android.util.Log;import android.view.LayoutInflater;import android.view.Menu;import android.view.View;import android.view.View.OnClickListener;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.Button;import android.widget.GridView;import com.carrey.customview.customview.CustomView;/** * 非同步載入的兩種方式:AsyncTask與ThreadPool * @author carrey * */public class MainActivity extends Activity implements ThreadPoolTaskBitmap.CallBack {/** 伺服器位址 */public static String webServerStr;private static final String TAG = "MainActivity";private LayoutInflater inflater;private Button btnAsync;private Button btnPool;private GridView gridView;private GridAdapter adapter;/** 載入方式 */private int loadWay;private static final int LOAD_ASYNC = 1;private static final int LOAD_POOL = 2;private ThreadPoolManager poolManager;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);loadWay = LOAD_ASYNC;webServerStr = getResources().getString(R.string.web_server);inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);btnAsync = (Button) findViewById(R.id.btn_async);btnAsync.setOnClickListener(new AsyncButtonClick());btnPool = (Button) findViewById(R.id.btn_pool);btnPool.setOnClickListener(new PoolButtonClick());gridView = (GridView) findViewById(R.id.gridview);adapter = new GridAdapter();gridView.setAdapter(adapter);poolManager = new ThreadPoolManager(ThreadPoolManager.TYPE_FIFO, 5);}private class AsyncButtonClick implements OnClickListener {@Overridepublic void onClick(View v) {loadWay = LOAD_ASYNC;adapter.notifyDataSetChanged();}}private class PoolButtonClick implements OnClickListener {@Overridepublic void onClick(View v) {loadWay = LOAD_POOL;adapter.notifyDataSetChanged();}}private class GridAdapter extends BaseAdapter {private Bitmap mBackgroundBitmap;public GridAdapter() {mBackgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.item_bg);}@Overridepublic int getCount() {return 999;}@Overridepublic Object getItem(int position) {return null;}@Overridepublic long getItemId(int position) {return 0;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder holder = null;if (convertView == null) {holder = new ViewHolder();convertView = inflater.inflate(R.layout.item, null);holder.customView = (CustomView) convertView.findViewById(R.id.customview);convertView.setTag(holder);} else {holder = (ViewHolder) convertView.getTag();}holder.customView.position = position;holder.customView.setImageBitmap(null);holder.customView.setBackgroundBitmap(mBackgroundBitmap);if (loadWay == LOAD_ASYNC) {holder.customView.setTitleText("AsyncTask");holder.customView.setSubTitleText("position: " + position);new AsyncLoadTask(holder.customView).execute(position);} else if (loadWay == LOAD_POOL) {holder.customView.setTitleText("ThreadPool");holder.customView.setSubTitleText("position: " + position);poolManager.start();String imageUrl = ImageHelper.getImageUrl(webServerStr, position);poolManager.addAsyncTask(new ThreadPoolTaskBitmap(imageUrl, MainActivity.this, position, holder.customView));}return convertView;}}static class ViewHolder {CustomView customView;}@Overrideprotected void onDestroy() {poolManager.stop();super.onDestroy();}@Overridepublic boolean onCreateOptionsMenu(Menu menu) {// Inflate the menu; this adds items to the action bar if it is present.getMenuInflater().inflate(R.menu.activity_main, menu);return true;}@Overridepublic void onReady(String url, Bitmap bitmap, int position, CustomView view) {Log.i(TAG, "thread pool done task: " + url);if (view.position == position) {view.setImageBitmap(bitmap);}}}
在文章開頭我們提到過,我對CustomView做了一些修改,下面做一些說明:
我在控制項裡添加了一個mDrawableBackground屬性,這個Drawable會在渲染圖片內容之前渲染,非同步載入的時候我會首先將其設定為一個空白的背景,這樣在圖片載入完成之前我們就會先看到一個白色的背景(或者你自訂一個沙漏或者時鐘之類的圖片),給使用者一種視覺上的延時效果。
除此之外我添加了一個pisition屬性,並在最終重新整理UI的時候用來做判斷,分別位於線程池實現方式中的任務單元執行到最後的回調實現和AsyncTask實現方式的onPostExecute中,如下的代碼:
@Overridepublic void onReady(String url, Bitmap bitmap, int position, CustomView view) {Log.i(TAG, "thread pool done task: " + url);if (view.position == position) {view.setImageBitmap(bitmap);}}
@Overrideprotected void onPostExecute(Pair<Integer, Bitmap> result) {if (result.first == view.position) {view.setImageBitmap(result.second);}}
為什麼要做這樣一個判斷呢?這是因為BaseAdapter的convertView是不停複用的,如果我們的滑動非常快,那就會存在這樣一種情況,有一個convertView還沒有載入完,就會第二次複用了,如果這個時候第二次載入慢於第一次,那結果就會被第一次覆蓋,這樣就不準確了,所以我們要加一個判斷,以確保重新整理的準確。
最後貼出源碼下載,感興趣的同學可以下載對比,歡迎留言交流。
源碼下載