Listview的使用與最佳化(中),listview使用最佳化
上篇文章簡單地介紹了listview的使用和最佳化,都是一些常見的最佳化技巧。但是listview最佳化還有一些重要的問題,那就是圖片載入,非同步載入的最佳化,因為圖片佔用記憶體較大,listview在滑動過程中很容易產生OOM的現象,下面我來給大家解釋一片非同步載入的最佳化思路。
總的來說有一下幾個最佳化思路:
1,對Imageview使用setTag()方法來解決圖片錯位問題,這個Tag中設定的是圖片的url,然後在載入的時候取得這個url和要載入那position中的url對比,如果不相同就載入,相同就是複用以前的就不載入了
2,對於要載入的圖片資源,先在記憶體緩衝中找(原始的方法是使用SoftRefrence,最新的方法是使用android提供的Lrucache),如果找不到,則在本機快取(可以使用DiskLrucache類)中找(也就是讀取原先下載過的本地圖片),還找不到,就開啟非同步線程去下載圖片,下載以後,儲存在本地,記憶體緩衝也保留一份引用
3,在非同步線程中,先測量需要的圖片大小,按比例縮放
4,使用一個Map儲存非同步線程的引用,key->value為url->AsyncTask,這樣可以避免已經開啟了線程去載入圖片,但是還沒有載入完時,又重複開啟線程去載入圖片的情況
5,在快速滑動的時候不載入圖片,暫停所有圖片載入線程,一旦停下來,結束不可見圖片的載入線程,繼續可見圖片的載入線程
下面都是我摘取的網上的一些例子,我分別介紹它們來說明上述的最佳化思路
第一個例子:
public class MemoryCache {private static final String TAG = "MemoryCache";// 放入緩衝時是個同步操作// LinkedHashMap構造方法的最後一個參數true代表這個map裡的元素將按照最近使用次數由少到多排列,即LRU// 這樣的好處是如果要將緩衝中的元素替換,則先遍曆出最近最少使用的元素來替換以提高效率private Map<String, Bitmap> cache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(10, 1.5f, true));// 緩衝中圖片所佔用的位元組,初始0,將通過此變數嚴格控制緩衝所佔用的堆記憶體private long size = 0;// current allocated size// 緩衝只能佔用的最大堆記憶體private long limit = 1000000;// max memory in bytespublic MemoryCache() {// use 25% of available heap sizesetLimit(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();}}也可以使用SoftReference,代碼會簡單很多,但是我推薦上面的方法。
public class MemoryCache {private Map<String, SoftReference<Bitmap>> cache = Collections.synchronizedMap(new HashMap<String, SoftReference<Bitmap>>());public Bitmap get(String id) {if (!cache.containsKey(id))return null;SoftReference<Bitmap> ref = cache.get(id);return ref.get();}public void put(String id, Bitmap bitmap) {cache.put(id, new SoftReference<Bitmap>(bitmap));}public void clear() {cache.clear();}}
下面是檔案快取類的代碼FileCache.java:
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");elsecacheDir = 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();}}
最後最重要的載入圖片的類,ImageLoader.java:
public class ImageLoader {MemoryCache memoryCache = new MemoryCache();FileCache fileCache;private Map<ImageView, String> imageViews = Collections.synchronizedMap(new WeakHashMap<ImageView, String>());// 線程池ExecutorService executorService;public ImageLoader(Context context) {fileCache = new FileCache(context);executorService = Executors.newFixedThreadPool(5);}// 當進入listview時預設的圖片,可換成你自己的預設圖片final int stub_id = R.drawable.stub;// 最主要的方法public void DisplayImage(String url, ImageView imageView) {imageViews.put(imageView, url);// 先從記憶體緩衝中尋找Bitmap bitmap = memoryCache.get(url);if (bitmap != null)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 f = fileCache.getFile(url);// 先從檔案快取中尋找是否有Bitmap b = decodeFile(f);if (b != null)return b;// 最後從指定的url中下載圖片try {Bitmap bitmap = null;URL imageUrl = new URL(url);HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection();conn.setConnectTimeout(30000);conn.setReadTimeout(30000);conn.setInstanceFollowRedirects(true);InputStream is = conn.getInputStream();OutputStream os = new FileOutputStream(f);CopyStream(is, os);os.close();bitmap = decodeFile(f);return bitmap;} catch (Exception ex) {ex.printStackTrace();return null;}}// decode這個圖片並且按比例縮放以減少記憶體消耗,虛擬機器對每張圖片的緩衝大小也是有限制的private Bitmap decodeFile(File f) {try {// decode image sizeBitmapFactory.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 inSampleSizeBitmapFactory.Options o2 = new BitmapFactory.Options();o2.inSampleSize = scale;return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);} catch (FileNotFoundException e) {}return null;}// Task for the queueprivate class PhotoToLoad {public String url;public ImageView imageView;public PhotoToLoad(String u, ImageView i) {url = u;imageView = i;}}class PhotosLoader implements Runnable {PhotoToLoad photoToLoad;PhotosLoader(PhotoToLoad photoToLoad) {this.photoToLoad = photoToLoad;}@Overridepublic void run() {if (imageViewReused(photoToLoad))return;Bitmap bmp = getBitmap(photoToLoad.url);memoryCache.put(photoToLoad.url, bmp);if (imageViewReused(photoToLoad))return;BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);// 更新的操作放在UI線程中Activity a = (Activity) photoToLoad.imageView.getContext();a.runOnUiThread(bd);}}/** * 防止圖片錯位 * * @param photoToLoad * @return */boolean imageViewReused(PhotoToLoad photoToLoad) {String tag = imageViews.get(photoToLoad.imageView);if (tag == null || !tag.equals(photoToLoad.url))return true;return false;}// 用於在UI線程中更新介面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);elsephotoToLoad.imageView.setImageResource(stub_id);}}public void clearCache() {memoryCache.clear();fileCache.clear();}public static void CopyStream(InputStream is, OutputStream os) {final int buffer_size = 1024;try {byte[] bytes = new byte[buffer_size];for (;;) {int count = is.read(bytes, 0, buffer_size);if (count == -1)break;os.write(bytes, 0, count);}} catch (Exception ex) {}}}
上面代碼的思路是這樣的,首先是一個MemoryCache類,用來緩衝圖片應用到記憶體。這個類包含一個Collectiosn.synchronizedMap(new LinkedHashMap<String,Bitmap>(10,1.5f,true))對象,這個對象就是用來儲存url和對應的bitmap的,也就是緩衝,最後一個參數設定為true的原因,是代表這個map裡的元素將按照最近使用次數由少到多排列,即LRU。這樣的好處是如果要將緩衝中的元素替換,則先遍曆出最近最少使用的元素來替換以提高效率 。
另外設定一個緩衝的最大值limit,和一個初始值size=0。每次添加圖片緩衝,Size就增加相應大小,如果增加以後大小超過limit,就遍曆LinkedHashMap清楚使用次數最少的緩衝,同時減小size值,直到size<limit。
作者還舉了一個使用SoftReference的例子,這樣做的好處是android會自動替我們回收適當的bitmap緩衝。
接下來是檔案快取,如果有SD卡則在SD卡中建一個LazyList的目錄存放緩衝的圖片,沒有SD卡就放在系統的緩衝目錄中,將url的hashCode作為緩衝的檔案名稱。這個類只是根據url名建立並返回了一個File類,沒有真正的緩衝圖片,圖片緩衝在ImageLoader類中,不過這個類要擷取FileCache返回的File來做FileOutputStream的目的地.
最後是負責的ImageLoader,這個類有一個線程池,用於管理下載線程。另外有一個WeakHashMap<ImageView, String>用於儲存imageview引用和記錄Tag,用於圖片更新。它先檢查緩衝,沒有則開啟一個線程去下載,下載以後圖片儲存到緩衝(記憶體,檔案),然後縮放映像比例,返回一個合適大小的bitmap,最後開啟一個線程去跟新UI(方式是imagview.getContext()擷取對應的context,然後context調用runOnUIThread()方法)。
另外,在下載線程開啟前,圖片下載完成後,跟新UI前,都通過WeakHashMap<ImageView, String>擷取下載圖片的Tag與對應要設定圖片imageview的tag比較,防止圖片錯位。
上述程式碼完成了基本的最佳化思路,甚至使用了一個自己定義的緩衝類MemoryCache,使管理變得更加清晰,同時有檔案快取,也通過imagview->url的方式避免了圖片錯位,還開啟了非同步線程下載圖片,但是又開啟了一個UI線程去跟新UI。
缺點是開啟了UI線程去更新UI,浪費了資源,其實這個可以使用定義一個回調介面實現。另外也沒有考慮到重複開啟下載線程的問題。
第二個例子:
先貼上主方法的代碼:
package cn.wangmeng.test;import java.io.IOException;import java.io.InputStream;import java.lang.ref.SoftReference;import java.net.MalformedURLException;import java.net.URL;import java.util.HashMap;import android.graphics.drawable.Drawable;import android.os.Handler;import android.os.Message;public class AsyncImageLoader { private HashMap<String, SoftReference<Drawable>> imageCache; public AsyncImageLoader() { imageCache = new HashMap<String, SoftReference<Drawable>>(); } public Drawable loadDrawable(final String imageUrl, final ImageCallback imageCallback) { if (imageCache.containsKey(imageUrl)) { SoftReference<Drawable> softReference = imageCache.get(imageUrl); Drawable drawable = softReference.get(); if (drawable != null) { return drawable; } } final Handler handler = new Handler() { public void handleMessage(Message message) { imageCallback.imageLoaded((Drawable) message.obj, imageUrl); } }; new Thread() { @Override public void run() { Drawable drawable = loadImageFromUrl(imageUrl); imageCache.put(imageUrl, new SoftReference<Drawable>(drawable)); Message message = handler.obtainMessage(0, drawable); handler.sendMessage(message); } }.start(); return null; } public static Drawable loadImageFromUrl(String url) {URL m;InputStream i = null;try {m = new URL(url);i = (InputStream) m.getContent();} catch (MalformedURLException e1) {e1.printStackTrace();} catch (IOException e) {e.printStackTrace();}Drawable d = Drawable.createFromStream(i, "src");return d;} public interface ImageCallback { public void imageLoaded(Drawable imageDrawable, String imageUrl); }}
以上代碼是實現非同步擷取圖片的主方法,SoftReference是軟引用,是為了更好的為了系統回收變數,重複的URL直接返回已有的資源,實現回呼函數,讓資料成功後,更新到UI線程。
幾個輔助類檔案:
package cn.wangmeng.test;public class ImageAndText { private String imageUrl; private String text; public ImageAndText(String imageUrl, String text) { this.imageUrl = imageUrl; this.text = text; } public String getImageUrl() { return imageUrl; } public String getText() { return text; }}
package cn.wangmeng.test;import android.view.View;import android.widget.ImageView;import android.widget.TextView;public class ViewCache { private View baseView; private TextView textView; private ImageView imageView; public ViewCache(View baseView) { this.baseView = baseView; } public TextView getTextView() { if (textView == null) { textView = (TextView) baseView.findViewById(R.id.text); } return textView; } public ImageView getImageView() { if (imageView == null) { imageView = (ImageView) baseView.findViewById(R.id.image); } return imageView; }}
ViewCache是輔助擷取adapter的子項目布局
package cn.wangmeng.test;import java.util.List;import cn.wangmeng.test.AsyncImageLoader.ImageCallback;import android.app.Activity;import android.graphics.drawable.Drawable;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.ArrayAdapter;import android.widget.ImageView;import android.widget.ListView;import android.widget.TextView;public class ImageAndTextListAdapter extends ArrayAdapter<ImageAndText> { private ListView listView; private AsyncImageLoader asyncImageLoader; public ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts, ListView listView) { super(activity, 0, imageAndTexts); this.listView = listView; asyncImageLoader = new AsyncImageLoader(); } public View getView(int position, View convertView, ViewGroup parent) { Activity activity = (Activity) getContext(); // Inflate the views from XML View rowView = convertView; ViewCache viewCache; if (rowView == null) { LayoutInflater inflater = activity.getLayoutInflater(); rowView = inflater.inflate(R.layout.image_and_text_row, null); viewCache = new ViewCache(rowView); rowView.setTag(viewCache); } else { viewCache = (ViewCache) rowView.getTag(); } ImageAndText imageAndText = getItem(position); // Load the image and set it on the ImageView String imageUrl = imageAndText.getImageUrl(); ImageView imageView = viewCache.getImageView(); imageView.setTag(imageUrl); Drawable cachedImage = asyncImageLoader.loadDrawable(imageUrl, new ImageCallback() { public void imageLoaded(Drawable imageDrawable, String imageUrl) { ImageView imageViewByTag = (ImageView) listView.findViewWithTag(imageUrl); if (imageViewByTag != null) { imageViewByTag.setImageDrawable(imageDrawable); } } });if (cachedImage == null) {imageView.setImageResource(R.drawable.default_image);}else{imageView.setImageDrawable(cachedImage);} // Set the text on the TextView TextView textView = viewCache.getTextView(); textView.setText(imageAndText.getText()); return rowView; }}
上述代碼的思路是這樣的:AsyncImageLoader類裡面,使用了一個HashMap<String, SoftReference<Drawable>>用來緩衝,然後有一個非同步下載線程,還有一個方法內部的handler,線程下載完成後,會發訊息給handler,然後handler調用回調介面imageCallback的imageLoaded()方法,這個方法是在adapter裡面實現的,所以也就是在主線程跟新UI了。
而ViewCache類的作用其實就是ViewHolder,ImageAndText是一個bean類。
在adapter中,使用mageView.setTag(imageUrl)為imageview提供一個唯一標識Url,所以先圖片下載完成以後,imageCallback的imageLoaded()方法中,就可以調用listview的findViewWithTag(imageUrl)來找到對應的imageview,從而不用擔心錯誤的問題,這個方法比較巧妙。
缺點是沒有實現檔案快取,另外也沒有解決出現多個線程下載同一張圖片的問題。