(轉)Android技術積累:圖片異步加載

當在ListView或GridView中要加載不少圖片時,很容易出現滑動時的卡頓現象,以及出現OOM致使FC(Force Close)。html

會出現卡頓現象主要是由於加載數據慢,要等數據加載完才能顯示出來。能夠經過將數據分頁顯示,以及將耗時的圖片加載用異步的方式和圖片緩存,這樣就能夠解決卡頓的問題。web

大部分開發者在ListView或GridView加載圖片時,都會在getView方法裏建立新的線程去異步加載圖片。然而,當屏幕快速向下滑動時,每一個劃過的Item都會調用getView一次,即會建立出不少線程,同一時間存在的線程太多,內存不夠用了,天然就會OOM了。要避免OOM,就得控制好線程的數量,因此加個線程池就很是有必要了。緩存

另外,當向下快速滑動屏幕時,也不必加載滑動過的全部圖片,只要加載滑動中止後當前屏幕的就足夠了。仔細觀察像微博、facebook或其餘優秀的app,滑動屏幕時未加載過的圖片是不會被加載的,當滑動中止後,也只加載當前屏幕內的圖片。網絡

那麼,接下來就討論實現的問題了。首先,圖片是須要緩存的,前一篇文章已經對圖片緩存作了總結(Android技術積累:圖片緩存管理),直接拿過來用就行。而後,線程池維護多少個線程比較合適呢?這個很難界定,線程太少CPU不能獲得充分利用,線程太多會下降性能,也加大了OOM的風險。線程池的最佳大小取決於可用處理器的數目以及工做隊列中的任務的性質。若在一個具備N個處理器的系統上只有一個工做隊列,其中所有是計算性質的任務,在線程池具備N或N+1個線程時通常會得到最大的CPU利用率。多線程

創建線程池的代碼以下:app

// 獲取當前系統的CPU數目
int cpuNums = Runtime.getRuntime().availableProcessors(); //根據系統資源狀況靈活定義線程池大小 ExecutorService executorService = Executors.newFixedThreadPool(cpuNums + 1); 

從內存緩存讀取圖片是很是快的,若是內存緩存中有圖片就能夠直接獲取,而不須要另起線程去異步加載,在內存緩存獲取不到時才往線程池裏添加新線程去加載圖片。既然是異步的,那就要知道獲取到的圖片是要加載到哪一個ImageView,能夠將ImageView保存起來。另外,爲了保證在整個應用中只有一個線程池,也不會出現多份緩存,圖片加載的工具類最好用單例模式。ListView或GridView滑動時不加載圖片,滑動中止後才加載圖片,所以加一個是否容許加載圖片的boolean變量。ListView或GridView初始化時是不滑動的,但也要加載圖片,因此boolean值變量初始應該爲true。異步

直接看圖片加載的工具類ImageLoader的完整代碼:ide

public class ImageLoader { private static ImageLoader instance; private ExecutorService executorService; //線程池 private ImageMemoryCache memoryCache; //內存緩存 private ImageFileCache fileCache; //文件緩存 private Map<String, ImageView> taskMap; //存聽任務 private boolean allowLoad = true; //是否容許加載圖片 private ImageLoader(Context context) { // 獲取當前系統的CPU數目 int cpuNums = Runtime.getRuntime().availableProcessors(); //根據系統資源狀況靈活定義線程池大小 this.executorService = Executors.newFixedThreadPool(cpuNums + 1); this.memoryCache = new ImageMemoryCache(context); this.fileCache = new ImageFileCache(); this.taskMap = new HashMap<String, ImageView>(); } /**  * 使用單例,保證整個應用中只有一個線程池和一分內存緩存和文件緩存  */ public static ImageLoader getInstance(Context context) { if (instance == null) instance = new ImageLoader(context); return instance; } /**  * 恢復爲初始可加載圖片的狀態  */ public void restore() { this.allowLoad = true; } /**  * 鎖住時不容許加載圖片  */ public void lock() { this.allowLoad = false; } /**  * 解鎖時加載圖片  */ public void unlock() { this.allowLoad = true; doTask(); } /**  * 添加任務  */ public void addTask(String url, ImageView img) { //先從內存緩存中獲取,取到直接加載 Bitmap bitmap = memoryCache.getBitmapFromCache(url); if (bitmap != null) { img.setImageBitmap(bitmap); } else { synchronized (taskMap) { /**  * 由於ListView或GridView的原理是用上面移出屏幕的item去填充下面新顯示的item,  * 這裏的img是item裏的內容,因此這裏的taskMap保存的始終是當前屏幕內的全部ImageView。  */ img.setTag(url); taskMap.put(Integer.toString(img.hashCode()), img); } if (allowLoad) { doTask(); } } } /**  * 加載存聽任務中的全部圖片  */ private void doTask() { synchronized (taskMap) { Collection<ImageView> con = taskMap.values(); for (ImageView i : con) { if (i != null) { if (i.getTag() != null) { loadImage((String) i.getTag(), i); } } } taskMap.clear(); } } private void loadImage(String url, ImageView img) { this.executorService.submit(new TaskWithResult(new TaskHandler(url, img), url)); } /*** 得到一個圖片,從三個地方獲取,首先是內存緩存,而後是文件緩存,最後從網絡獲取 ***/ private Bitmap getBitmap(String url) { // 從內存緩存中獲取圖片 Bitmap result = memoryCache.getBitmapFromCache(url); if (result == null) { // 文件緩存中獲取 result = fileCache.getImage(url); if (result == null) { // 從網絡獲取 result = ImageGetFromHttp.downloadBitmap(url); if (result != null) { fileCache.saveBitmap(result, url); memoryCache.addBitmapToCache(url, result); } } else { // 添加到內存緩存 memoryCache.addBitmapToCache(url, result); } } return result; } /*** 子線程任務 ***/ private class TaskWithResult implements Callable<String> { private String url; private Handler handler; public TaskWithResult(Handler handler, String url) { this.url = url; this.handler = handler; } @Override public String call() throws Exception { Message msg = new Message(); msg.obj = getBitmap(url); if (msg.obj != null) { handler.sendMessage(msg); } return url; } } /*** 完成消息 ***/ private class TaskHandler extends Handler { String url; ImageView img; public TaskHandler(String url, ImageView img) { this.url = url; this.img = img; } @Override public void handleMessage(Message msg) { /*** 查看ImageView須要顯示的圖片是否被改變 ***/ if (img.getTag().equals(url)) { if (msg.obj != null) { Bitmap bitmap = (Bitmap) msg.obj; img.setImageBitmap(bitmap); } } } } } 

有一點須要注意,要保證taskMap保存的始終只是當前屏幕內的全部ImageView,在ImageAdapter的getView方法裏必須使用ViewHolder模式,這樣才能保證item被重用時相應的ImageView也被重用。getView代碼相似以下:工具

@Override
public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = mInflater.inflate(R.layout.list_item, null); holder = new ViewHolder(); holder.text = (TextView) convertView.findViewById(R.id.text); holder.image = (ImageView) convertView.findViewById(R.id.img); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } ListItem item = mItems.get(position); //ListView的Item holder.text.setText(item.getText()); holder.image.setImageResource(R.drawable.default_img); //設置默認圖片 mImageLoader.addTask(item.getImgUrl(), holder.image); //添加任務 return convertView; } static class ViewHolder { TextView text; ImageView image; } 

ListView或GridView滑動時就須要鎖住不容許加載圖片,滑動中止後解鎖加載圖片。所以,給ListView或GridView添加一個OnScrollListener,代碼以下:性能

mImageLoader = ImageLoader.getInstance(context); mListView.setOnScrollListener(onScrollListener); OnScrollListener onScrollListener = new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { switch (scrollState) { case OnScrollListener.SCROLL_STATE_FLING: mImageLoader.lock(); break; case OnScrollListener.SCROLL_STATE_IDLE: mImageLoader.unlock(); break; case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL: mImageLoader.lock(); break; default: break; } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }; 

至此,全部關鍵代碼就全都列出來了。

異步加載圖片,關鍵就在於三點:一、緩存;二、線程池;三、只加載當前屏幕

相關文章
相關標籤/搜索