Android之ListView圖片加載錯位問題解決

之前做一個類似於今日頭條的app,遇到 ListView 加載圖片錯位的問題,其更本原因是 convertView 的重用,以下一張圖可以說明此問題:




當重用 convertView 時,最初一屏顯示 7 條記錄, getView 被調用 7 次,創建了 7 個 convertView. 當 Item1 劃出屏幕, Item8 進入屏幕時,這時沒有爲 Item8 創建新的 view 實例, Item8 複用的是Item1 的 view 如果沒有異步不會有任何問題,雖然 Item8 和 Item1 指向的是同一個 view,但滑到Item8 時刷上了 Item8 的數據,這時 Item1 的數據和 Item8 是一樣的,因爲它們指向的是同一塊內存,但 Item1 已滾出了屏幕你看不見。當 Item1 再次可見時這塊 view 又涮上了 Item1 的數據。

但當有異步下載時就有問題了,假設 Item1 的圖片下載的比較慢,Item8 的圖片下載的比較快,你滾上去使 Item8 可見,這時 Item8 先顯示它自己下載的圖片沒錯,但等到 Item1 的圖片也下載完時你發現Item8 的圖片也變成了 Item1 的圖片,因爲它們複用的是同一個 view。 如果 Item1 的圖片下載的比Item8 的圖片快, Item1 先刷上自己下載的圖片,這時你滑下去,Item8 的圖片還沒下載完, Item8會先顯示 Item1 的圖片,因爲它們是同一快內存,當 Item8 自己的圖片下載完後 Item8 的圖片又刷成了自己的,你再滑上去使 Item1 可見, Item1 的圖片也會和 Item8 的圖片是一樣的,因爲它們指向的是同一塊內存。

解決方案一:

package com.lzn.jnews.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.util.Log;
import android.widget.ImageView;

import com.lzn.jnews.R;
import com.lzn.jnews.net.HttpFetcher;

/**
 * 圖片加載類(防止錯位)
 * 
 * @author lzn
 *
 */
public class ImageLoader {
	// 內存緩存
	private MemoryCache mMemoryCache;
	// 文件緩存
	private FileCache mFileCache;
	private Map<ImageView, String> mImageViewMap = Collections
			.synchronizedMap(new WeakHashMap<ImageView, String>());
	private ExecutorService executorService;
	
	// 默認的圖片id
	private int default_image_id = R.drawable.default_bg;

	public ImageLoader(Context context) {
		mMemoryCache = new MemoryCache();
		mFileCache = new FileCache(context);
		executorService = Executors.newFixedThreadPool(10);
	}

	// 最主要的方法
	public void loadImage(String url, ImageView imageView) {
		mImageViewMap.put(imageView, url);
		if (StringUtil.isEmpty(url)) {
			imageView.setImageResource(default_image_id);
			return;
		}
		// 先從內存緩存中查找
		Bitmap bitmap = mMemoryCache.get(url);
		if (bitmap != null) {
			if (mImageViewMap.get(imageView).equals(url)) {
				Log.d("ImageLoader", "get from cache");
				imageView.setImageBitmap(bitmap);
			}
		} else {
			// 若沒有的話則開啓新線程加載圖片
			queuePhoto(url, imageView);
			// imageView.setImageResource(stub_id);
		}
	}

	private void queuePhoto(String url, ImageView imageView) {
		PhotoToLoad p = new PhotoToLoad(url, imageView);
		executorService.submit(new PhotosLoader(p));
	}

	private Bitmap getBitmap(String url) {
		File file = mFileCache.getFile(url);
		// 先從文件緩存中查找是否有
		Bitmap bitmap = decodeFile(file);
		if (bitmap != null) {
			Log.d("ImageLoader", "get from file");
			return bitmap;
		}
		// 最後從指定的url中下載圖片
		try {
			Log.d("ImageLoader", "get from url: " + url);
			bitmap = new HttpFetcher().getBitmap(url);
			// 將圖片寫入文件
			bitmap.compress(CompressFormat.PNG, 100, new FileOutputStream(file));
			return bitmap;
		} catch (Exception ex) {
			ex.printStackTrace();
			return null;
		}
	}

	// decode這個圖片並且按比例縮放以減少內存消耗,虛擬機對每張圖片的緩存大小也是有限制的
	private Bitmap decodeFile(File f) {
		try {
			// decode image size
			BitmapFactory.Options o = new BitmapFactory.Options();
			o.inJustDecodeBounds = true;
			BitmapFactory.decodeStream(new FileInputStream(f), null, o);

			// Find the correct scale value. It should be the power of 2.
			final int REQUIRED_SIZE = 70;
			int width_tmp = o.outWidth, height_tmp = o.outHeight;
			int scale = 1;
			while (true) {
				if (width_tmp / 2 < REQUIRED_SIZE
						|| height_tmp / 2 < REQUIRED_SIZE)
					break;
				width_tmp /= 2;
				height_tmp /= 2;
				scale *= 2;
			}

			// decode with inSampleSize
			BitmapFactory.Options o2 = new BitmapFactory.Options();
			o2.inSampleSize = scale;
			return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
		} catch (FileNotFoundException e) {
		}
		return null;
	}
	
	/**
	 * 防止圖片錯位
	 * 
	 * @param photoToLoad
	 * @return
	 */
	private boolean imageViewReused(PhotoToLoad photoToLoad) {
		String tag = mImageViewMap.get(photoToLoad.imageView);
		if (tag == null || !tag.equals(photoToLoad.url))
			return true;
		return false;
	}
	
	public void clearCache() {
		mMemoryCache.clear();
		mFileCache.clear();
	}

	// 隊列任務
	private class PhotoToLoad {
		public String url;
		public ImageView imageView;

		public PhotoToLoad(String u, ImageView i) {
			url = u;
			imageView = i;
		}
	}

	// 加載圖片線程
	private class PhotosLoader implements Runnable {
		PhotoToLoad photoToLoad;

		PhotosLoader(PhotoToLoad photoToLoad) {
			this.photoToLoad = photoToLoad;
		}

		@Override
		public void run() {
			if (imageViewReused(photoToLoad)) {
				return;
			}
			Bitmap bitmap = getBitmap(photoToLoad.url);
			mMemoryCache.put(photoToLoad.url, bitmap);
			if (imageViewReused(photoToLoad)) {
				return;
			}
			BitmapDisplayer bd = new BitmapDisplayer(bitmap, photoToLoad);
			// 更新的操作放在UI線程中
			Activity a = (Activity) photoToLoad.imageView.getContext();
			a.runOnUiThread(bd);
		}
	}

	// 更新UI線程
	private class BitmapDisplayer implements Runnable {
		Bitmap bitmap;
		PhotoToLoad photoToLoad;

		public BitmapDisplayer(Bitmap b, PhotoToLoad p) {
			bitmap = b;
			photoToLoad = p;
		}

		public void run() {
			if (imageViewReused(photoToLoad)) {
				return;
			}
			if (bitmap != null) {
				photoToLoad.imageView.setImageBitmap(bitmap);
			} else {
				photoToLoad.imageView.setImageResource(default_image_id);
			}
		}
	}
	
}

其中 MemoryCache 和 FileCache 可以自定義實現,其中一種實現方案如下:

package com.lzn.jnews.util;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

import android.graphics.Bitmap;
import android.util.Log;

public class MemoryCache {

	private static final String TAG = "MemoryCache";
	// 放入緩存時是個同步操作
	// LinkedHashMap構造方法的最後一個參數true代表這個map裏的元素將按照最近使用次數由少到多排列,即LRU
	// 這樣的好處是如果要將緩存中的元素替換,則先遍歷出最近最少使用的元素來替換以提高效率
	private static Map<String, Bitmap> cache = Collections
			.synchronizedMap(new LinkedHashMap<String, Bitmap>(10, 1.5f, true));
	// 緩存中圖片所佔用的字節,初始0,將通過此變量嚴格控制緩存所佔用的堆內存
	// current allocated size
	private long size = 0;
	// 緩存只能佔用的最大堆內存
	private long limit = 1000000;// max memory in bytes

	public MemoryCache() {
		// use 25% of available heap size
		setLimit(Runtime.getRuntime().maxMemory() / 4);
	}

	public void setLimit(long new_limit) {
		limit = new_limit;
		Log.i(TAG, "MemoryCache will use up to " + limit / 1024. / 1024. + "MB");
	}

	public Bitmap get(String id) {
		try {
			if (!cache.containsKey(id))
				return null;
			return cache.get(id);
		} catch (NullPointerException ex) {
			return null;
		}
	}

	public void put(String id, Bitmap bitmap) {
		try {
			if (cache.containsKey(id))
				size -= getSizeInBytes(cache.get(id));
			cache.put(id, bitmap);
			size += getSizeInBytes(bitmap);
			checkSize();
		} catch (Throwable th) {
			th.printStackTrace();
		}
	}

	/**
	 * 嚴格控制堆內存,如果超過將首先替換最近最少使用的那個圖片緩存
	 * 
	 */
	private void checkSize() {
		Log.i(TAG, "cache size=" + size + " length=" + cache.size());
		if (size > limit) {
			// 先遍歷最近最少使用的元素
			Iterator<Entry<String, Bitmap>> iter = cache.entrySet().iterator();
			while (iter.hasNext()) {
				Entry<String, Bitmap> entry = iter.next();
				size -= getSizeInBytes(entry.getValue());
				iter.remove();
				if (size <= limit)
					break;
			}
			Log.i(TAG, "Clean cache. New size " + cache.size());
		}
	}

	public void clear() {
		cache.clear();
	}

	/**
	 * 圖片佔用的內存
	 * 
	 * @param bitmap
	 * @return
	 */
	long getSizeInBytes(Bitmap bitmap) {
		if (bitmap == null)
			return 0;
		return bitmap.getRowBytes() * bitmap.getHeight();
	}
}

package com.lzn.jnews.util;

import java.io.File;

import android.content.Context;

public class FileCache {
	// 緩存目錄
	private File cacheDir;

	public FileCache(Context context) {
		// 如果有SD卡則在SD卡中建一個LazyList的目錄存放緩存的圖片
		// 沒有SD卡就放在系統的緩存目錄中
		if (android.os.Environment.getExternalStorageState().equals(
				android.os.Environment.MEDIA_MOUNTED))
			cacheDir = new File(
					android.os.Environment.getExternalStorageDirectory(),
					"LazyList");
		else
			cacheDir = context.getCacheDir();
		if (!cacheDir.exists())
			cacheDir.mkdirs();
	}

	public File getFile(String url) {
		// 將url的hashCode作爲緩存的文件名
		String filename = String.valueOf(url.hashCode());
		// Another possible solution
		// String filename = URLEncoder.encode(url);
		File f = new File(cacheDir, filename);
		return f;

	}

	public void clear() {
		File[] files = cacheDir.listFiles();
		if (files == null)
			return;
		for (File f : files)
			f.delete();
	}
}

這個問題糾結了好久,終於有一種解決方案了,還是很開心的,後續如有別的解決方案 還會再補充。

此解決方案參考如下兩個博客,再次感謝!

http://1002878825-qq-com.iteye.com/blog/1610006,http://www.cnblogs.com/lesliefang/p/3619223.html
發佈了32 篇原創文章 · 獲贊 9 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章