上篇文章简单地介绍了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 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(); } }
也可以使用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"); 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(); } }
最后最重要的加载图片的类,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 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; } // Task for the queue private 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; } @Override public 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); else photoToLoad.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,从而不用担心错误的问题,这个方法比较巧妙。
缺点是没有实现文件缓存,另外也没有解决出现多个线程下载同一张图片的问题。