listview异步加载图片出现乱序的情况


原文链接  http://blog.csdn.net/guolin_blog/article/details/45586553 侵删

ListView在借助RecycleBin机制的帮助下,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,可以实现加载多条数据而不会出现oom的情况。

每当有新的元素进入界面时就会回调getView()方法,而在getView()方法中会开启异步请求从网络上获取图片,注意网络操作都是比较耗时的,也就是说当我们快速滑动ListView的时候就很有可能出现这样一种情况,某一个位置上的元素进入屏幕后开始从网络上请求图片,但是还没等图片下载完成,它就又被移出了屏幕。这种情况下会产生什么样的现象呢?根据ListView的工作原理,被移出屏幕的控件将会很快被新进入屏幕的元素重新利用起来,而如果在这个时候刚好前面发起的图片请求有了响应,就会将刚才位置上的图片显示到当前位置上,因为虽然它们位置不同,但都是共用的同一个ImageView实例,这样就出现了图片乱序的情况。

解决方案一  使用findViewWithTag

findViewWithTag算是一种比较简单易懂的解决方案,其实早在 Android照片墙应用实现,再多的图片也不怕崩溃 这篇文章当中,我就采用了findViewWithTag来避免图片出现乱序的情况。那么这里我们先来看看怎么通过修改代码把这个问题解决掉,然后再研究一下findViewWithTag的工作原理。

使用findViewWithTag并不需要修改太多的代码,只需要改动ImageAdapter这一个类就可以了,如下所示:

[java]  view plain  copy
  1. /**  
  2.  * 原文地址: http://blog.csdn.net/guolin_blog/article/details/45586553  
  3.  * @author guolin  
  4.  */    
  5. public class ImageAdapter extends ArrayAdapter<String> {  
  6.       
  7.     private ListView mListView;   
  8.   
  9.     ......  
  10.   
  11.     @Override  
  12.     public View getView(int position, View convertView, ViewGroup parent) {  
  13.         if (mListView == null) {    
  14.             mListView = (ListView) parent;    
  15.         }   
  16.         String url = getItem(position);  
  17.         View view;  
  18.         if (convertView == null) {  
  19.             view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);  
  20.         } else {  
  21.             view = convertView;  
  22.         }  
  23.         ImageView image = (ImageView) view.findViewById(R.id.image);  
  24.         image.setImageResource(R.drawable.empty_photo);  
  25.         image.setTag(url);  
  26.         BitmapDrawable drawable = getBitmapFromMemoryCache(url);  
  27.         if (drawable != null) {  
  28.             image.setImageDrawable(drawable);  
  29.         } else {  
  30.             BitmapWorkerTask task = new BitmapWorkerTask();  
  31.             task.execute(url);  
  32.         }  
  33.         return view;  
  34.     }  
  35.   
  36.     ......  
  37.   
  38.     /** 
  39.      * 异步下载图片的任务。 
  40.      *  
  41.      * @author guolin 
  42.      */  
  43.     class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {  
  44.   
  45.         String imageUrl;   
  46.   
  47.         @Override  
  48.         protected BitmapDrawable doInBackground(String... params) {  
  49.             imageUrl = params[0];  
  50.             // 在后台开始下载图片  
  51.             Bitmap bitmap = downloadBitmap(imageUrl);  
  52.             BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);  
  53.             addBitmapToMemoryCache(imageUrl, drawable);  
  54.             return drawable;  
  55.         }  
  56.   
  57.         @Override  
  58.         protected void onPostExecute(BitmapDrawable drawable) {  
  59.             ImageView imageView = (ImageView) mListView.findViewWithTag(imageUrl);    
  60.             if (imageView != null && drawable != null) {    
  61.                 imageView.setImageDrawable(drawable);    
  62.             }   
  63.         }  
  64.   
  65.         ......  
  66.   
  67.     }  
  68.   
  69. }  

改动的地方就只有这么多,那么我们来分析一下。由于使用findViewWithTag必须要有ListView的实例才行,那么我们在Adapter中怎样才能拿到ListView的实例呢?其实如果你仔细通读了上一篇文章就能知道,getView()方法中传入的第三个参数其实就是ListView的实例,那么这里我们定义一个全局变量mListView,然后在getView()方法中判断它是否为空,如果为空就把parent这个参数赋值给它。


另外在getView()方法中我们还做了一个操作,就是调用了ImageView的setTag()方法,并把当前位置图片的URL地址作为参数传了进去,这个是为后续的findViewWithTag()方法做准备。


最后,我们修改了BitmapWorkerTask的构造函数,这里不再通过构造函数把ImageView的实例传进去了,而是在onPostExecute()方法当中通过ListView的findVIewWithTag()方法来去获取ImageView控件的实例。获取到控件实例后判断下是否为空,如果不为空就让图片显示到控件上。


解决方案二  使用弱引用关联

虽然这里我给这种解决方案起名叫弱引用关联,但实际上弱引用只是辅助手段而已,最主要的还是关联,这种解决方案的本质是要让ImageView和BitmapWorkerTask之间建立一个双向关联,互相持有对方的引用,再通过适当的逻辑判断来解决图片乱序问题,然后为了防止出现内存泄漏的情况,双向关联要使用弱引用的方式建立。相比于第一种解决方案,第二种解决方案要明显复杂不少,但在性能和效率方面都会有更好的表现。


我们仍然只需要改动ImageAdapter中的代码,但这次改动的地方比较多,所以我就把ImageAdapter中的全部代码都贴出来了,如下所示:

[java]  view plain  copy
  1. /**  
  2.  * 原文地址: http://blog.csdn.net/guolin_blog/article/details/45586553  
  3.  * @author guolin  
  4.  */    
  5. public class ImageAdapter extends ArrayAdapter<String> {  
  6.       
  7.     private ListView mListView;   
  8.       
  9.     private Bitmap mLoadingBitmap;  
  10.   
  11.     /** 
  12.      * 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉。 
  13.      */  
  14.     private LruCache<String, BitmapDrawable> mMemoryCache;  
  15.   
  16.     public ImageAdapter(Context context, int resource, String[] objects) {  
  17.         super(context, resource, objects);  
  18.         mLoadingBitmap = BitmapFactory.decodeResource(context.getResources(),  
  19.                 R.drawable.empty_photo);  
  20.         // 获取应用程序最大可用内存  
  21.         int maxMemory = (int) Runtime.getRuntime().maxMemory();  
  22.         int cacheSize = maxMemory / 8;  
  23.         mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {  
  24.             @Override  
  25.             protected int sizeOf(String key, BitmapDrawable drawable) {  
  26.                 return drawable.getBitmap().getByteCount();  
  27.             }  
  28.         };  
  29.     }  
  30.   
  31.     @Override  
  32.     public View getView(int position, View convertView, ViewGroup parent) {  
  33.         if (mListView == null) {    
  34.             mListView = (ListView) parent;    
  35.         }   
  36.         String url = getItem(position);  
  37.         View view;  
  38.         if (convertView == null) {  
  39.             view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);  
  40.         } else {  
  41.             view = convertView;  
  42.         }  
  43.         ImageView image = (ImageView) view.findViewById(R.id.image);  
  44.         BitmapDrawable drawable = getBitmapFromMemoryCache(url);  
  45.         if (drawable != null) {  
  46.             image.setImageDrawable(drawable);  
  47.         } else if (cancelPotentialWork(url, image)) {  
  48.             BitmapWorkerTask task = new BitmapWorkerTask(image);  
  49.             AsyncDrawable asyncDrawable = new AsyncDrawable(getContext()  
  50.                     .getResources(), mLoadingBitmap, task);  
  51.             image.setImageDrawable(asyncDrawable);  
  52.             task.execute(url);  
  53.         }  
  54.         return view;  
  55.     }  
  56.       
  57.     /** 
  58.      * 自定义的一个Drawable,让这个Drawable持有BitmapWorkerTask的弱引用。 
  59.      */  
  60.     class AsyncDrawable extends BitmapDrawable {  
  61.   
  62.         private WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;  
  63.   
  64.         public AsyncDrawable(Resources res, Bitmap bitmap,  
  65.                 BitmapWorkerTask bitmapWorkerTask) {  
  66.             super(res, bitmap);  
  67.             bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(  
  68.                     bitmapWorkerTask);  
  69.         }  
  70.   
  71.         public BitmapWorkerTask getBitmapWorkerTask() {  
  72.             return bitmapWorkerTaskReference.get();  
  73.         }  
  74.   
  75.     }  
  76.       
  77.     /** 
  78.      * 获取传入的ImageView它所对应的BitmapWorkerTask。 
  79.      */  
  80.     private BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {  
  81.         if (imageView != null) {  
  82.             Drawable drawable = imageView.getDrawable();  
  83.             if (drawable instanceof AsyncDrawable) {  
  84.                 AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;  
  85.                 return asyncDrawable.getBitmapWorkerTask();  
  86.             }  
  87.         }  
  88.         return null;  
  89.     }  
  90.       
  91.     /** 
  92.      * 取消掉后台的潜在任务,当认为当前ImageView存在着一个另外图片请求任务时 
  93.      * ,则把它取消掉并返回true,否则返回false。 
  94.      */  
  95.     public boolean cancelPotentialWork(String url, ImageView imageView) {  
  96.         BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);  
  97.         if (bitmapWorkerTask != null) {  
  98.             String imageUrl = bitmapWorkerTask.imageUrl;  
  99.             if (imageUrl == null || !imageUrl.equals(url)) {  
  100.                 bitmapWorkerTask.cancel(true);  
  101.             } else {  
  102.                 return false;  
  103.             }  
  104.         }  
  105.         return true;  
  106.     }  
  107.   
  108.     /** 
  109.      * 将一张图片存储到LruCache中。 
  110.      *  
  111.      * @param key 
  112.      *            LruCache的键,这里传入图片的URL地址。 
  113.      * @param drawable 
  114.      *            LruCache的值,这里传入从网络上下载的BitmapDrawable对象。 
  115.      */  
  116.     public void addBitmapToMemoryCache(String key, BitmapDrawable drawable) {  
  117.         if (getBitmapFromMemoryCache(key) == null) {  
  118.             mMemoryCache.put(key, drawable);  
  119.         }  
  120.     }  
  121.   
  122.     /** 
  123.      * 从LruCache中获取一张图片,如果不存在就返回null。 
  124.      *  
  125.      * @param key 
  126.      *            LruCache的键,这里传入图片的URL地址。 
  127.      * @return 对应传入键的BitmapDrawable对象,或者null。 
  128.      */  
  129.     public BitmapDrawable getBitmapFromMemoryCache(String key) {  
  130.         return mMemoryCache.get(key);  
  131.     }  
  132.   
  133.     /** 
  134.      * 异步下载图片的任务。 
  135.      *  
  136.      * @author guolin 
  137.      */  
  138.     class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {  
  139.   
  140.         String imageUrl;   
  141.           
  142.         private WeakReference<ImageView> imageViewReference;  
  143.           
  144.         public BitmapWorkerTask(ImageView imageView) {    
  145.             imageViewReference = new WeakReference<ImageView>(imageView);  
  146.         }    
  147.   
  148.         @Override  
  149.         protected BitmapDrawable doInBackground(String... params) {  
  150.             imageUrl = params[0];  
  151.             // 在后台开始下载图片  
  152.             Bitmap bitmap = downloadBitmap(imageUrl);  
  153.             BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), bitmap);  
  154.             addBitmapToMemoryCache(imageUrl, drawable);  
  155.             return drawable;  
  156.         }  
  157.   
  158.         @Override  
  159.         protected void onPostExecute(BitmapDrawable drawable) {  
  160.             ImageView imageView = getAttachedImageView();  
  161.             if (imageView != null && drawable != null) {    
  162.                 imageView.setImageDrawable(drawable);    
  163.             }   
  164.         }  
  165.           
  166.         /** 
  167.          * 获取当前BitmapWorkerTask所关联的ImageView。 
  168.          */  
  169.         private ImageView getAttachedImageView() {  
  170.             ImageView imageView = imageViewReference.get();  
  171.             BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);  
  172.             if (this == bitmapWorkerTask) {  
  173.                 return imageView;  
  174.             }  
  175.             return null;  
  176.         }  
  177.   
  178.         /** 
  179.          * 建立HTTP请求,并获取Bitmap对象。 
  180.          *  
  181.          * @param imageUrl 
  182.          *            图片的URL地址 
  183.          * @return 解析后的Bitmap对象 
  184.          */  
  185.         private Bitmap downloadBitmap(String imageUrl) {  
  186.             Bitmap bitmap = null;  
  187.             HttpURLConnection con = null;  
  188.             try {  
  189.                 URL url = new URL(imageUrl);  
  190.                 con = (HttpURLConnection) url.openConnection();  
  191.                 con.setConnectTimeout(5 * 1000);  
  192.                 con.setReadTimeout(10 * 1000);  
  193.                 bitmap = BitmapFactory.decodeStream(con.getInputStream());  
  194.             } catch (Exception e) {  
  195.                 e.printStackTrace();  
  196.             } finally {  
  197.                 if (con != null) {  
  198.                     con.disconnect();  
  199.                 }  
  200.             }  
  201.             return bitmap;  
  202.         }  
  203.   
  204.     }  
  205.   
  206. }  
那么我们一点点开始解析。首先刚才说到的,ImageView和BitmapWorkerTask之间要建立一个双向的弱引用关联,上述代码中已经建立好了。ImageView中可以获取到它所对应的BitmapWorkerTask,而BitmapWorkerTask也可以获取到它所对应的ImageView。


下面来看一下这个双向弱引用关联是怎么建立的。BitmapWorkerTask指向ImageView的弱引用关联比较简单,就是在BitmapWorkerTask中加入一个构造函数,并在构造函数中要求传入ImageView这个参数。不过我们不再直接持有ImageView的引用,而是使用WeakReference对ImageView进行了一层包装,这样就OK了。


但是ImageView指向BitmapWorkerTask的弱引用关联就没这么容易了,因为我们很难将BitmapWorkerTask的一个弱引用直接设置到ImageView当中。这该怎么办呢?这里使用了一个比较巧的方法,就是借助自定义Drawable的方式来实现。可以看到,我们自定义了一个AsyncDrawable类并让它继承自BitmapDrawable,然后重写了AsyncDrawable的构造函数,在构造函数中要求把BitmapWorkerTask传入,然后在这里给它包装了一层弱引用。那么现在AsyncDrawable指向BitmapWorkerTask的关联已经有了,但是ImageView指向BitmapWorkerTask的关联还不存在,怎么办呢?很简单,让ImageView和AsyncDrawable再关联一下就可以了。可以看到,在getView()方法当中,我们调用了ImageView的setImageDrawable()方法把AsyncDrawable设置了进去,那么ImageView就可以通过getDrawable()方法获取到和它关联的AsyncDrawable,然后再借助AsyncDrawable就可以获取到BitmapWorkerTask了。这样ImageView指向BitmapWorkerTask的弱引用关联也成功建立。


现在双向弱引用的关联已经建立好了,接下来就是逻辑判断的工作了。那么怎样通过逻辑判断来避免图片出现乱序的情况呢?这里我们引入了两个方法,一个是getBitmapWorkerTask()方法,这个方法可以根据传入的ImageView来获取到它对应的BitmapWorkerTask,内部的逻辑就是先获取ImageView对应的AsyncDrawable,再获取AsyncDrawable对应的BitmapWorkerTask。另一个是getAttachedImageView()方法,这个方法会获取当前BitmapWorkerTask所关联的ImageView,然后调用getBitmapWorkerTask()方法来获取该ImageView所对应的BitmapWorkerTask,最后判断,如果获取到的BitmapWorkerTask等于this,也就是当前的BitmapWorkerTask,那么就将ImageView返回,否则就返回null。最后,在onPostExecute()方法当中,只需要使用getAttachedImageView()方法获取到的ImageView来显示图片就可以了。


那么为什么做了这个逻辑判断之后,图片乱序的问题就可以得到解决呢?其实最主要的奥秘就是在getAttachedImageView()方法当中,它会使用当前BitmapWorkerTask所关联的ImageView来反向获取这个ImageView所关联的BitmapWorkerTask,然后用这两个BitmapWorkerTask做对比,如果发现是同一个BitmapWorkerTask才会返回ImageView,否则就返回null。那么什么情况下这两个BitmapWorkerTask才会不同呢?比如说某个图片被移出了屏幕,它的ImageView被另外一个新进入屏幕的图片重用了,那么就会给这个ImageView关联一个新的BitmapWorkerTask,这种情况下,上一个BitmapWorkerTask和新的BitmapWorkerTask肯定就不相等了,这时getAttachedImageView()方法会返回null,而我们又判断ImageView等于null的话是不会设置图片的,因此就不会出现图片乱序的情况了。


除此之外还有另外一个方法非常值得大家注意,就是cancelPotentialWork()方法,这个方法可以大大提高整个ListView图片加载的工作效率。这个方法接收两个参数,一个图片的url,一个ImageView。看一下它的内部逻辑,首先它也是调用了getBitmapWorkerTask()方法来获取传入的ImageView所对应的BitmapWorkerTask,接下来拿BitmapWorkerTask中的imageUrl和传入的url做比较,如果两个url不等的话就调用BitmapWorkerTask的cancel()方法,然后返回true,如果两个url相等的话就返回false。


那么这段逻辑是什么意思呢?其实并不复杂,两个url做比对时,如果发现是相同的,说明请求的是同一张图片,那么直接返回false,这样就不会再去启动BitmapWorkerTask来请求图片,而如果两个url不相同,说明这个ImageView被另外一张图片重新利用了,这个时候就调用了BitmapWorkerTask的cancel()方法把之前的请求取消掉,然后重新启动BitmapWorkerTask来去请求新图片。有了这个操作保护之后,就可以把一些已经移出屏幕的无效的图片请求过滤掉,从而整体提升ListView加载图片的工作效率。

解决方案三  使用NetworkImageView

前面两种解决方案都需要我们自己去做额外的逻辑处理,因为ImageView本身是不能自动解决这个问题的,但是如果我们使用NetworkImageView这个控件的话就非常简单了,它自身就已经考虑到了这个问题,我们直接使用它就可以了,不用做任何额外的处理也不会出现图片乱序的情况。


NetworkImageView是Volley当中提供的控件,对于这个控件我之前专门写过一篇博客来讲解,还不熟悉这个控件的朋友可以先去阅读 Android Volley完全解析(二),使用Volley加载网络图片


下面我们看一下如何用NetworkImageView来解决这个问题,首先需要修改一下image_item.xml文件,因为我们已经不再使用ImageView控件了,代码如下所示:

[html]  view plain  copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <com.android.volley.toolbox.NetworkImageView  
  7.         android:id="@+id/image"  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="120dp"  
  10.         android:src="@drawable/empty_photo"   
  11.         android:scaleType="fitXY"/>  
  12.   
  13. </LinearLayout>  
很简单,只是把ImageView替换成了NetworkImageView。然后修改ImageAdapter中的代码,如下所示:
[java]  view plain  copy
  1. /** 
  2.  * 原文地址: http://blog.csdn.net/guolin_blog/article/details/45586553 
  3.  * @author guolin 
  4.  */  
  5. public class ImageAdapter extends ArrayAdapter<String> {  
  6.       
  7.     ImageLoader mImageLoader;  
  8.   
  9.     public ImageAdapter(Context context, int resource, String[] objects) {  
  10.         super(context, resource, objects);  
  11.         RequestQueue queue = Volley.newRequestQueue(context);  
  12.         mImageLoader = new ImageLoader(queue, new BitmapCache());  
  13.     }  
  14.   
  15.     @Override  
  16.     public View getView(int position, View convertView, ViewGroup parent) {  
  17.         String url = getItem(position);  
  18.         View view;  
  19.         if (convertView == null) {  
  20.             view = LayoutInflater.from(getContext()).inflate(R.layout.image_item, null);  
  21.         } else {  
  22.             view = convertView;  
  23.         }  
  24.         NetworkImageView image = (NetworkImageView) view.findViewById(R.id.image);  
  25.         image.setDefaultImageResId(R.drawable.empty_photo);  
  26.         image.setErrorImageResId(R.drawable.empty_photo);  
  27.         image.setImageUrl(url, mImageLoader);  
  28.         return view;  
  29.     }  
  30.   
  31.     /** 
  32.      * 使用LruCache来缓存图片 
  33.      */  
  34.     public class BitmapCache implements ImageCache {  
  35.   
  36.         private LruCache<String, Bitmap> mCache;  
  37.   
  38.         public BitmapCache() {  
  39.             // 获取应用程序最大可用内存  
  40.             int maxMemory = (int) Runtime.getRuntime().maxMemory();  
  41.             int cacheSize = maxMemory / 8;  
  42.             mCache = new LruCache<String, Bitmap>(cacheSize) {  
  43.                 @Override  
  44.                 protected int sizeOf(String key, Bitmap bitmap) {  
  45.                     return bitmap.getRowBytes() * bitmap.getHeight();  
  46.                 }  
  47.             };  
  48.         }  
  49.   
  50.         @Override  
  51.         public Bitmap getBitmap(String url) {  
  52.             return mCache.get(url);  
  53.         }  
  54.   
  55.         @Override  
  56.         public void putBitmap(String url, Bitmap bitmap) {  
  57.             mCache.put(url, bitmap);  
  58.         }  
  59.   
  60.     }  
  61.   
  62. }  
没错,就是这么简单,一共60行左右的代码搞定一切!我们不需要自己再去写一个BitmapWorkerTask来处理图片的下载和显示,也不需要自己再去管理LruCache的逻辑,一切NetworkImageView都帮我们做好了。至于上面的代码我就不再做解释了,因为实在是太简单了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值