listview优化(中)

时间:2021-09-08 17:46:33

1,对Imageview使用setTag()方法来解决图片错位问题,这个Tag中设置的是图片的url,然后在加载的时候取得这个url和要加载那position中的url对比,如果不相同就加载,相同就是复用以前的就不加载了

2,对于要加载的图片资源,先在内存缓存中找(原始的方法是使用SoftRefrence,最新的方法是使用android提供的Lrucache),如果找不到,则在本地缓存(可以使用DiskLrucache类)中找(也就是读取原先下载过的本地图片),还找不到,就开启异步线程去下载图片,下载以后,保存在本地,内存缓存也保留一份引用

3,在为imagview装载图片时,先测量需要的图片大小,按比例缩放

4,使用一个Map保存异步线程的引用,key->value为url->AsyncTask,这样可以避免已经开启了线程去加载图片,但是还没有加载完时,又重复开启线程去加载图片的情况

5,在快速滑动的时候不加载图片,取消所有图片加载线程,一旦停下来,继续可见图片的加载线程

下面都是我摘取的网上的一些例子,我分别介绍它们来说明上述的优化思路

第一个例子:

[java] view
plain
copylistview优化(中)listview优化(中)
  1. public class MemoryCache {
  2. private static final String TAG = "MemoryCache";
  3. // 放入缓存时是个同步操作
  4. // LinkedHashMap构造方法的最后一个参数true代表这个map里的元素将按照最近使用次数由少到多排列,即LRU
  5. // 这样的好处是如果要将缓存中的元素替换,则先遍历出最近最少使用的元素来替换以提高效率
  6. private Map<String, Bitmap> cache = Collections
  7. .synchronizedMap(new LinkedHashMap<String, Bitmap>(10, 1.5f, true));
  8. // 缓存中图片所占用的字节,初始0,将通过此变量严格控制缓存所占用的堆内存
  9. private long size = 0;// current allocated size
  10. // 缓存只能占用的最大堆内存
  11. private long limit = 1000000;// max memory in bytes
  12. public MemoryCache() {
  13. // use 25% of available heap size
  14. setLimit(Runtime.getRuntime().maxMemory() / 4);
  15. }
  16. public void setLimit(long new_limit) {
  17. limit = new_limit;
  18. Log.i(TAG, "MemoryCache will use up to " + limit / 1024. / 1024. + "MB");
  19. }
  20. public Bitmap get(String id) {
  21. try {
  22. if (!cache.containsKey(id))
  23. return null;
  24. return cache.get(id);
  25. } catch (NullPointerException ex) {
  26. return null;
  27. }
  28. }
  29. public void put(String id, Bitmap bitmap) {
  30. try {
  31. if (cache.containsKey(id))
  32. size -= getSizeInBytes(cache.get(id));
  33. cache.put(id, bitmap);
  34. size += getSizeInBytes(bitmap);
  35. checkSize();
  36. } catch (Throwable th) {
  37. th.printStackTrace();
  38. }
  39. }
  40. /**
  41. * 严格控制堆内存,如果超过将首先替换最近最少使用的那个图片缓存
  42. *
  43. */
  44. private void checkSize() {
  45. Log.i(TAG, "cache size=" + size + " length=" + cache.size());
  46. if (size > limit) {
  47. // 先遍历最近最少使用的元素
  48. Iterator<Entry<String, Bitmap>> iter = cache.entrySet().iterator();
  49. while (iter.hasNext()) {
  50. Entry<String, Bitmap> entry = iter.next();
  51. size -= getSizeInBytes(entry.getValue());
  52. iter.remove();
  53. if (size <= limit)
  54. break;
  55. }
  56. Log.i(TAG, "Clean cache. New size " + cache.size());
  57. }
  58. }
  59. public void clear() {
  60. cache.clear();
  61. }
  62. /**
  63. * 图片占用的内存
  64. *
  65. * @param bitmap
  66. * @return
  67. */
  68. long getSizeInBytes(Bitmap bitmap) {
  69. if (bitmap == null)
  70. return 0;
  71. return bitmap.getRowBytes() * bitmap.getHeight();
  72. }
  73. }

也可以使用SoftReference,代码会简单很多,但是我推荐上面的方法。

[java] view
plain
copylistview优化(中)listview优化(中)
  1. public class MemoryCache {
  2. private Map<String, SoftReference<Bitmap>> cache = Collections
  3. .synchronizedMap(new HashMap<String, SoftReference<Bitmap>>());
  4. public Bitmap get(String id) {
  5. if (!cache.containsKey(id))
  6. return null;
  7. SoftReference<Bitmap> ref = cache.get(id);
  8. return ref.get();
  9. }
  10. public void put(String id, Bitmap bitmap) {
  11. cache.put(id, new SoftReference<Bitmap>(bitmap));
  12. }
  13. public void clear() {
  14. cache.clear();
  15. }
  16. }

下面是文件缓存类的代码FileCache.java:

[java] view
plain
copylistview优化(中)listview优化(中)
  1. public class FileCache {
  2. private File cacheDir;
  3. public FileCache(Context context) {
  4. // 如果有SD卡则在SD卡中建一个LazyList的目录存放缓存的图片
  5. // 没有SD卡就放在系统的缓存目录中
  6. if (android.os.Environment.getExternalStorageState().equals(
  7. android.os.Environment.MEDIA_MOUNTED))
  8. cacheDir = new File(
  9. android.os.Environment.getExternalStorageDirectory(),
  10. "LazyList");
  11. else
  12. cacheDir = context.getCacheDir();
  13. if (!cacheDir.exists())
  14. cacheDir.mkdirs();
  15. }
  16. public File getFile(String url) {
  17. // 将url的hashCode作为缓存的文件名
  18. String filename = String.valueOf(url.hashCode());
  19. // Another possible solution
  20. // String filename = URLEncoder.encode(url);
  21. File f = new File(cacheDir, filename);
  22. return f;
  23. }
  24. public void clear() {
  25. File[] files = cacheDir.listFiles();
  26. if (files == null)
  27. return;
  28. for (File f : files)
  29. f.delete();
  30. }
  31. }

最后最重要的加载图片的类,ImageLoader.java:

[java] view
plain
copylistview优化(中)listview优化(中)
  1. public class ImageLoader {
  2. MemoryCache memoryCache = new MemoryCache();
  3. FileCache fileCache;
  4. private Map<ImageView, String> imageViews = Collections
  5. .synchronizedMap(new WeakHashMap<ImageView, String>());
  6. // 线程池
  7. ExecutorService executorService;
  8. public ImageLoader(Context context) {
  9. fileCache = new FileCache(context);
  10. executorService = Executors.newFixedThreadPool(5);
  11. }
  12. // 当进入listview时默认的图片,可换成你自己的默认图片
  13. final int stub_id = R.drawable.stub;
  14. // 最主要的方法
  15. public void DisplayImage(String url, ImageView imageView) {
  16. imageViews.put(imageView, url);
  17. // 先从内存缓存中查找
  18. Bitmap bitmap = memoryCache.get(url);
  19. if (bitmap != null)
  20. imageView.setImageBitmap(bitmap);
  21. else {
  22. // 若没有的话则开启新线程加载图片
  23. queuePhoto(url, imageView);
  24. imageView.setImageResource(stub_id);
  25. }
  26. }
  27. private void queuePhoto(String url, ImageView imageView) {
  28. PhotoToLoad p = new PhotoToLoad(url, imageView);
  29. executorService.submit(new PhotosLoader(p));
  30. }
  31. private Bitmap getBitmap(String url) {
  32. File f = fileCache.getFile(url);
  33. // 先从文件缓存中查找是否有
  34. Bitmap b = decodeFile(f);
  35. if (b != null)
  36. return b;
  37. // 最后从指定的url中下载图片
  38. try {
  39. Bitmap bitmap = null;
  40. URL imageUrl = new URL(url);
  41. HttpURLConnection conn = (HttpURLConnection) imageUrl
  42. .openConnection();
  43. conn.setConnectTimeout(30000);
  44. conn.setReadTimeout(30000);
  45. conn.setInstanceFollowRedirects(true);
  46. InputStream is = conn.getInputStream();
  47. OutputStream os = new FileOutputStream(f);
  48. CopyStream(is, os);
  49. os.close();
  50. bitmap = decodeFile(f);
  51. return bitmap;
  52. } catch (Exception ex) {
  53. ex.printStackTrace();
  54. return null;
  55. }
  56. }
  57. // decode这个图片并且按比例缩放以减少内存消耗,虚拟机对每张图片的缓存大小也是有限制的
  58. private Bitmap decodeFile(File f) {
  59. try {
  60. // decode image size
  61. BitmapFactory.Options o = new BitmapFactory.Options();
  62. o.inJustDecodeBounds = true;
  63. BitmapFactory.decodeStream(new FileInputStream(f), null, o);
  64. // Find the correct scale value. It should be the power of 2.
  65. final int REQUIRED_SIZE = 70;
  66. int width_tmp = o.outWidth, height_tmp = o.outHeight;
  67. int scale = 1;
  68. while (true) {
  69. if (width_tmp / 2 < REQUIRED_SIZE
  70. || height_tmp / 2 < REQUIRED_SIZE)
  71. break;
  72. width_tmp /= 2;
  73. height_tmp /= 2;
  74. scale *= 2;
  75. }
  76. // decode with inSampleSize
  77. BitmapFactory.Options o2 = new BitmapFactory.Options();
  78. o2.inSampleSize = scale;
  79. return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
  80. } catch (FileNotFoundException e) {
  81. }
  82. return null;
  83. }
  84. // Task for the queue
  85. private class PhotoToLoad {
  86. public String url;
  87. public ImageView imageView;
  88. public PhotoToLoad(String u, ImageView i) {
  89. url = u;
  90. imageView = i;
  91. }
  92. }
  93. class PhotosLoader implements Runnable {
  94. PhotoToLoad photoToLoad;
  95. PhotosLoader(PhotoToLoad photoToLoad) {
  96. this.photoToLoad = photoToLoad;
  97. }
  98. @Override
  99. public void run() {
  100. if (imageViewReused(photoToLoad))
  101. return;
  102. Bitmap bmp = getBitmap(photoToLoad.url);
  103. memoryCache.put(photoToLoad.url, bmp);
  104. if (imageViewReused(photoToLoad))
  105. return;
  106. BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);
  107. // 更新的操作放在UI线程中
  108. Activity a = (Activity) photoToLoad.imageView.getContext();
  109. a.runOnUiThread(bd);
  110. }
  111. }
  112. /**
  113. * 防止图片错位
  114. *
  115. * @param photoToLoad
  116. * @return
  117. */
  118. boolean imageViewReused(PhotoToLoad photoToLoad) {
  119. String tag = imageViews.get(photoToLoad.imageView);
  120. if (tag == null || !tag.equals(photoToLoad.url))
  121. return true;
  122. return false;
  123. }
  124. // 用于在UI线程中更新界面
  125. class BitmapDisplayer implements Runnable {
  126. Bitmap bitmap;
  127. PhotoToLoad photoToLoad;
  128. public BitmapDisplayer(Bitmap b, PhotoToLoad p) {
  129. bitmap = b;
  130. photoToLoad = p;
  131. }
  132. public void run() {
  133. if (imageViewReused(photoToLoad))
  134. return;
  135. if (bitmap != null)
  136. photoToLoad.imageView.setImageBitmap(bitmap);
  137. else
  138. photoToLoad.imageView.setImageResource(stub_id);
  139. }
  140. }
  141. public void clearCache() {
  142. memoryCache.clear();
  143. fileCache.clear();
  144. }
  145. public static void CopyStream(InputStream is, OutputStream os) {
  146. final int buffer_size = 1024;
  147. try {
  148. byte[] bytes = new byte[buffer_size];
  149. for (;;) {
  150. int count = is.read(bytes, 0, buffer_size);
  151. if (count == -1)
  152. break;
  153. os.write(bytes, 0, count);
  154. }
  155. } catch (Exception ex) {
  156. }
  157. }
  158. }



上面代码的思路是这样的,首先是一个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,浪费了资源,其实这个可以使用定义一个回调接口实现。另外也没有考虑到重复开启下载线程的问题。

第二个例子:

先贴上主方法的代码:

[java] view
plain
copylistview优化(中)listview优化(中)
  1. package cn.wangmeng.test;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.lang.ref.SoftReference;
  5. import java.net.MalformedURLException;
  6. import java.net.URL;
  7. import java.util.HashMap;
  8. import android.graphics.drawable.Drawable;
  9. import android.os.Handler;
  10. import android.os.Message;
  11. public class AsyncImageLoader {
  12. private HashMap<String, SoftReference<Drawable>> imageCache;
  13. public AsyncImageLoader() {
  14. imageCache = new HashMap<String, SoftReference<Drawable>>();
  15. }
  16. public Drawable loadDrawable(final String imageUrl, final ImageCallback imageCallback) {
  17. if (imageCache.containsKey(imageUrl)) {
  18. SoftReference<Drawable> softReference = imageCache.get(imageUrl);
  19. Drawable drawable = softReference.get();
  20. if (drawable != null) {
  21. return drawable;
  22. }
  23. }
  24. final Handler handler = new Handler() {
  25. public void handleMessage(Message message) {
  26. imageCallback.imageLoaded((Drawable) message.obj, imageUrl);
  27. }
  28. };
  29. new Thread() {
  30. @Override
  31. public void run() {
  32. Drawable drawable = loadImageFromUrl(imageUrl);
  33. imageCache.put(imageUrl, new SoftReference<Drawable>(drawable));
  34. Message message = handler.obtainMessage(0, drawable);
  35. handler.sendMessage(message);
  36. }
  37. }.start();
  38. return null;
  39. }
  40. public static Drawable loadImageFromUrl(String url) {
  41. URL m;
  42. InputStream i = null;
  43. try {
  44. m = new URL(url);
  45. i = (InputStream) m.getContent();
  46. } catch (MalformedURLException e1) {
  47. e1.printStackTrace();
  48. } catch (IOException e) {
  49. e.printStackTrace();
  50. }
  51. Drawable d = Drawable.createFromStream(i, "src");
  52. return d;
  53. }
  54. public interface ImageCallback {
  55. public void imageLoaded(Drawable imageDrawable, String imageUrl);
  56. }
  57. }

以上代码是实现异步获取图片的主方法,SoftReference是软引用,是为了更好的为了系统回收变量,重复的URL直接返回已有的资源,实现回调函数,让数据成功后,更新到UI线程。

几个辅助类文件:

[java] view
plain
copylistview优化(中)listview优化(中)
  1. package cn.wangmeng.test;
  2. public class ImageAndText {
  3. private String imageUrl;
  4. private String text;
  5. public ImageAndText(String imageUrl, String text) {
  6. this.imageUrl = imageUrl;
  7. this.text = text;
  8. }
  9. public String getImageUrl() {
  10. return imageUrl;
  11. }
  12. public String getText() {
  13. return text;
  14. }
  15. }
[java] view
plain
copylistview优化(中)listview优化(中)
  1. package cn.wangmeng.test;
  2. import android.view.View;
  3. import android.widget.ImageView;
  4. import android.widget.TextView;
  5. public class ViewCache {
  6. private View baseView;
  7. private TextView textView;
  8. private ImageView imageView;
  9. public ViewCache(View baseView) {
  10. this.baseView = baseView;
  11. }
  12. public TextView getTextView() {
  13. if (textView == null) {
  14. textView = (TextView) baseView.findViewById(R.id.text);
  15. }
  16. return textView;
  17. }
  18. public ImageView getImageView() {
  19. if (imageView == null) {
  20. imageView = (ImageView) baseView.findViewById(R.id.image);
  21. }
  22. return imageView;
  23. }
  24. }

ViewCache是辅助获取adapter的子元素布局

[java] view
plain
copylistview优化(中)listview优化(中)
  1. package cn.wangmeng.test;
  2. import java.util.List;
  3. import cn.wangmeng.test.AsyncImageLoader.ImageCallback;
  4. import android.app.Activity;
  5. import android.graphics.drawable.Drawable;
  6. import android.view.LayoutInflater;
  7. import android.view.View;
  8. import android.view.ViewGroup;
  9. import android.widget.ArrayAdapter;
  10. import android.widget.ImageView;
  11. import android.widget.ListView;
  12. import android.widget.TextView;
  13. public class ImageAndTextListAdapter extends ArrayAdapter<ImageAndText> {
  14. private ListView listView;
  15. private AsyncImageLoader asyncImageLoader;
  16. public ImageAndTextListAdapter(Activity activity, List<ImageAndText> imageAndTexts, ListView listView) {
  17. super(activity, 0, imageAndTexts);
  18. this.listView = listView;
  19. asyncImageLoader = new AsyncImageLoader();
  20. }
  21. public View getView(int position, View convertView, ViewGroup parent) {
  22. Activity activity = (Activity) getContext();
  23. // Inflate the views from XML
  24. View rowView = convertView;
  25. ViewCache viewCache;
  26. if (rowView == null) {
  27. LayoutInflater inflater = activity.getLayoutInflater();
  28. rowView = inflater.inflate(R.layout.image_and_text_row, null);
  29. viewCache = new ViewCache(rowView);
  30. rowView.setTag(viewCache);
  31. } else {
  32. viewCache = (ViewCache) rowView.getTag();
  33. }
  34. ImageAndText imageAndText = getItem(position);
  35. // Load the image and set it on the ImageView
  36. String imageUrl = imageAndText.getImageUrl();
  37. ImageView imageView = viewCache.getImageView();
  38. imageView.setTag(imageUrl);
  39. Drawable cachedImage = asyncImageLoader.loadDrawable(imageUrl, new ImageCallback() {
  40. public void imageLoaded(Drawable imageDrawable, String imageUrl) {
  41. ImageView imageViewByTag = (ImageView) listView.findViewWithTag(imageUrl);
  42. if (imageViewByTag != null) {
  43. imageViewByTag.setImageDrawable(imageDrawable);
  44. }
  45. }
  46. });
  47. if (cachedImage == null) {
  48. imageView.setImageResource(R.drawable.default_image);
  49. }else{
  50. imageView.setImageDrawable(cachedImage);
  51. }
  52. // Set the text on the TextView
  53. TextView textView = viewCache.getTextView();
  54. textView.setText(imageAndText.getText());
  55. return rowView;
  56. }
  57. }

上述代码的思路是这样的: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,从而不用担心错误的问题,这个方法比较巧妙。

缺点是没有实现文件缓存,另外也没有解决出现多个线程下载同一张图片的问题。