android中ListView异步加载图片时的图片错位问题解决方案

时间:2021-05-10 20:15:01

Android中的ListView是一个非常常用的控件,但是它却并不像想象中的那么简单。特别是当你需要在ListView中展示大量网络图片的时候,处理不好轻则用户体验不佳,重则OOM,异步线程丢失或者图片错位。

关于其中的OOM和异步线程丢失的问题,是一个很庞大的话题,本人能力有限,无法说清,只有遇到的时候临时找原因,想办法解决了。但是对于图片错位,却是可以避免的,今天我们就来说一说ListView异步加载图片中的图片错位问题。

为什么会出现图片错位的问题呢?一般是重用了convertView导致的。如果你重用了convertView,此时convertView中的ImageViewid值是相等的,而我们在设置ImageView的图片时,是根据id来设置的,此时就出现了图片错位问题。这里童鞋们可以自己去测试一下,不重用convertView,也就是每次getView的时候,都使用findViewById(R.id.xx)去得到每一个ItemImageView,异步下载图片的方法也只是简单的开一个AsyncTask执行下载。在这种情况下,图片一般是不会产生错位的。原因很简单,认真读一读前面的内容就明白了。但是你如果真的在使用这种方法来使用getView的话,并且图片量比较大的时候,你程序的性能肯定不会好到哪里去了。因此,重用convertView还是很有必要的。

这里需要注意,convertView是否为null会根据ListView的中布局标签值的不同有区别,具体的内容请参见这两篇文章:

android listview 连续调用 getview问题分析及解决

[Android] ListView中getView的原理+如何在ListView中放置多个item

这也就是说,某种情况下你界面中的第一张和第二张图片之间就有可能产生错位,因为有可能第二个可见的ImageView就来自共用的convertView

处理像这种图片的异步加载的问题,我们的一般思路是:下载的图片根据图片名称存入到SDCard中,最新加载的图片存入到软引用中。我们在getView中给ImageView设置图片的时候,首先根据url,从软引用中读取图片数据;如果软引用中没用,则根据url(对应图片名)SDCard中读取图片数据;如果SDCard中也没有,则从网络上下载图片,在图片下载完成后,回调主线中的方法更新ImageView。下面我们就照着上面的思路,先把程序整出来再说吧。先看下效果图:

android中ListView异步加载图片时的图片错位问题解决方案

布局文件有两个,很简单,一个表示ListView(main.xml),一个表示ListView中的元素(single_data.xml),如下:

[java]  view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:tools="http://schemas.android.com/tools"  
  4.     android:layout_width="fill_parent"  
  5.     android:layout_height="fill_parent"  
  6.     android:orientation="vertical"  
  7.     android:background="@android:color/darker_gray"  
  8.     tools:context=".MainActivity" >  
  9.   
  10.     <ListView  
  11.         android:layout_width="fill_parent"  
  12.         android:layout_height="wrap_content"  
  13.         android:cacheColorHint="@null"  
  14.         android:id="@+id/listview"  
  15.          />  
  16.   
  17. </LinearLayout>  


 

[java]  view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="fill_parent"  
  4.     android:layout_height="wrap_content"  
  5.     android:background="@android:color/white"  
  6.      >  
  7.     <ImageView   
  8.         android:layout_width="150dp"  
  9.         android:layout_height="150dp"  
  10.         android:scaleType="fitXY"  
  11.         android:id="@+id/image_view"  
  12.         android:background="@drawable/ic_launcher"  
  13.         />  
  14.     <TextView   
  15.         android:layout_width="wrap_content"  
  16.         android:layout_height="wrap_content"  
  17.         android:layout_alignTop="@id/image_view"  
  18.         android:layout_alignBottom="@id/image_view"  
  19.         android:layout_marginLeft="20dp"  
  20.         android:layout_alignParentRight="true"  
  21.         android:gravity="center_vertical"  
  22.         android:layout_toRightOf="@id/image_view"  
  23.         android:singleLine="true"  
  24.         android:ellipsize="end"  
  25.         android:text="@string/hello"  
  26.         android:id="@+id/text_view"  
  27.         />  
  28.   
  29. </RelativeLayout>  


 

加入访问网络和读取,写入sdcard的权限。

[java]  view plain copy
  1. <uses-permission android:name="android.permission.INTERNET"/>  
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>  
  3. <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>  


接下来,我们来看看MainActivity.java。性能考虑,我们使用convertViewViewHolder来重用控件。这里涉及到比较关键的一步,我们会在getView的时候给ViewHolder中的ImageView设置tag,其值为要放置在该ImageView上的图片的url地址。这个tag很重要,在异步下载图片完成回调的方法中,我们使用findViewWithTag(String url)来找到ListView中对应的ImagView,然后给该ImageView设置图片即可。其他的就是设置adapter的一般操作了。

[java]  view plain copy
  1. public class MainActivity extends Activity {  
  2.     ListView mListView;  
  3.     ImageDownloader mDownloader;  
  4.     MyListAdapter myListAdapter;  
  5.     private static final String TAG = "MainActivity";  
  6.     int m_flag = 0;  
  7.     private static final String[] URLS = {  
  8.             //图片地址就不贴了,自己去这篇帖子中找吧:http://www.cnblogs.com/liongname/articles/2345087.html  
  9.             //其中有几张图片访问不了。  
  10.              };  
  11.   
  12.     @Override  
  13.     public void onCreate(Bundle savedInstanceState) {  
  14.         super.onCreate(savedInstanceState);  
  15.         setContentView(R.layout.main);  
  16.         Util.flag = 0;  
  17.         mListView = (ListView) findViewById(R.id.listview);  
  18.         myListAdapter = new MyListAdapter();  
  19.         mListView.setAdapter(myListAdapter);  
  20.     }  
  21.   
  22.     private class MyListAdapter extends BaseAdapter {  
  23.         private ViewHolder mHolder;  
  24.   
  25.         @Override  
  26.         public int getCount() {  
  27.             return URLS.length;  
  28.         }  
  29.   
  30.         @Override  
  31.         public Object getItem(int position) {  
  32.             return URLS[position];  
  33.         }  
  34.   
  35.         @Override  
  36.         public long getItemId(int position) {  
  37.             return position;  
  38.         }  
  39.   
  40.         @Override  
  41.         public View getView(int position, View convertView, ViewGroup parent) {  
  42.             //只有当convertView不存在的时候才去inflate子元素  
  43.             if (convertView == null) {  
  44.                 convertView = getLayoutInflater().inflate(R.layout.single_data,  
  45.                         null);  
  46.                  mHolder = new ViewHolder();  
  47.                  mHolder.mImageView = (ImageView) convertView.findViewById(R.id.image_view);  
  48.                  mHolder.mTextView = (TextView) convertView.findViewById(R.id.text_view);  
  49.                  convertView.setTag(mHolder);  
  50.             }else {  
  51.              mHolder = (ViewHolder) convertView.getTag();  
  52.              }  
  53.             final String url = URLS[position];  
  54.              mHolder.mTextView.setText(url != null ? url.substring(url.lastIndexOf("/") + 1) : "");  
  55.              mHolder.mImageView.setTag(URLS[position]);  
  56.             if (mDownloader == null) {  
  57.                 mDownloader = new ImageDownloader();  
  58.             }  
  59.             //这句代码的作用是为了解决convertView被重用的时候,图片预设的问题  
  60.             mHolder.mImageView.setImageResource(R.drawable.ic_launcher);  
  61.             if (mDownloader != null) {  
  62.                 //异步下载图片  
  63.                 mDownloader.imageDownload(url, mHolder.mImageView, "/yanbin",MainActivity.thisnew OnImageDownload() {  
  64.                             @Override  
  65.                             public void onDownloadSucc(Bitmap bitmap,  
  66.                                     String c_url,ImageView mimageView) {  
  67.                                 ImageView imageView = (ImageView) mListView.findViewWithTag(c_url);  
  68.                                 if (imageView != null) {  
  69.                                     imageView.setImageBitmap(bitmap);  
  70.                                     imageView.setTag("");  
  71.                                 }   
  72.                             }  
  73.                         });  
  74.             }  
  75.             return convertView;  
  76.   
  77.         }  
  78.   
  79.         /** 
  80.          * 使用ViewHolder来优化listview 
  81.          * @author yanbin 
  82.          * 
  83.          */  
  84.         private class ViewHolder {  
  85.             ImageView mImageView;  
  86.             TextView mTextView;  
  87.         }  
  88.     }  
  89. }  


上面的mDownloader.imageDownload()就是异步下载图片比较核心的方法,该方法在ImageDownloader.java类下。其中的五个参数分别为:要设置在当前ImageView 上的图片的url地址,当前ImageView,文件缓存地址,当前的activity以及图片回调接口。

ImageDownloader类中,我们首先根据url从软引用中获取图片,如果不存在,从sdcard中读取图片,如果还不存在,则启动一个AsyncTask异步下载图片。注意注意:这里我们做了一个这样的操作:用一个map将当前的url及其对应的MyAsyncTask存放起来了。由于getView会执行至少一次,这一步的操作是为了相同的url创建相同的AsyncTask。在onPostExecute()方法中,将该url对应的信息从map中删除,一定要记得执行这一步。看到很多的异步图片下载的例子中,重复创建AsyncTask都是普遍存在的,这里我们使用上面的思路解决掉了这一问题。更详细的代码自己看ImageDownloader.java类吧,首先给出OnImageDownload.java接口的代码:

[java]  view plain copy
  1. public interface OnImageDownload {  
  2.     void onDownloadSucc(Bitmap bitmap,String c_url,ImageView imageView);  
  3. }  


ImageDownloader.java的代码(有两百多行,拷贝到eclipse中看会舒服一点)

[java]  view plain copy
  1. public class ImageDownloader {  
  2.     private static final String TAG = "ImageDownloader";  
  3.     private HashMap<String, MyAsyncTask> map = new HashMap<String, MyAsyncTask>();  
  4.     private Map<String, SoftReference<Bitmap>> imageCaches = new HashMap<String, SoftReference<Bitmap>>();  
  5.     /** 
  6.      *  
  7.      * @param url 该mImageView对应的url 
  8.      * @param mImageView 
  9.      * @param path 文件存储路径 
  10.      * @param mActivity 
  11.      * @param download OnImageDownload回调接口,在onPostExecute()中被调用 
  12.      */  
  13.     public void imageDownload(String url,ImageView mImageView,String path,Activity mActivity,OnImageDownload download){  
  14.         SoftReference<Bitmap> currBitmap = imageCaches.get(url);  
  15.         Bitmap softRefBitmap = null;  
  16.         if(currBitmap != null){  
  17.             softRefBitmap = currBitmap.get();  
  18.         }  
  19.         String imageName = "";  
  20.         if(url != null){  
  21.             imageName = Util.getInstance().getImageName(url);  
  22.         }  
  23.         Bitmap bitmap = getBitmapFromFile(mActivity,imageName,path);  
  24.         //先从软引用中拿数据  
  25.         if(currBitmap != null && mImageView != null && softRefBitmap != null && url.equals(mImageView.getTag())){  
  26.             mImageView.setImageBitmap(softRefBitmap);  
  27.         }  
  28.         //软引用中没有,从文件中拿数据  
  29.         else if(bitmap != null && mImageView != null && url.equals(mImageView.getTag())){  
  30.             mImageView.setImageBitmap(bitmap);  
  31.         }  
  32.         //文件中也没有,此时根据mImageView的tag,即url去判断该url对应的task是否已经在执行,如果在执行,本次操作不创建新的线程,否则创建新的线程。  
  33.         else if(url != null && needCreateNewTask(mImageView)){  
  34.             MyAsyncTask task = new MyAsyncTask(url, mImageView, path,mActivity,download);  
  35.             if(mImageView != null){  
  36.                 Log.i(TAG, "执行MyAsyncTask --> " + Util.flag);  
  37.                 Util.flag ++;  
  38.                 task.execute();  
  39.                 //将对应的url对应的任务存起来  
  40.                 map.put(url, task);  
  41.             }  
  42.         }  
  43.     }  
  44.       
  45.     /** 
  46.      * 判断是否需要重新创建线程下载图片,如果需要,返回值为true。 
  47.      * @param url 
  48.      * @param mImageView 
  49.      * @return 
  50.      */  
  51.     private boolean needCreateNewTask(ImageView mImageView){  
  52.         boolean b = true;  
  53.         if(mImageView != null){  
  54.             String curr_task_url = (String)mImageView.getTag();  
  55.             if(isTasksContains(curr_task_url)){  
  56.                 b = false;  
  57.             }  
  58.         }  
  59.         return b;  
  60.     }  
  61.       
  62.     /** 
  63.      * 检查该url(最终反映的是当前的ImageView的tag,tag会根据position的不同而不同)对应的task是否存在 
  64.      * @param url 
  65.      * @return 
  66.      */  
  67.     private boolean isTasksContains(String url){  
  68.         boolean b = false;  
  69.         if(map != null && map.get(url) != null){  
  70.             b = true;  
  71.         }  
  72.         return b;  
  73.     }  
  74.       
  75.     /** 
  76.      * 删除map中该url的信息,这一步很重要,不然MyAsyncTask的引用会“一直”存在于map中 
  77.      * @param url 
  78.      */  
  79.     private void removeTaskFormMap(String url){  
  80.         if(url != null && map != null && map.get(url) != null){  
  81.             map.remove(url);  
  82.             System.out.println("当前map的大小=="+map.size());  
  83.         }  
  84.     }  
  85.       
  86.     /** 
  87.      * 从文件中拿图片 
  88.      * @param mActivity  
  89.      * @param imageName 图片名字 
  90.      * @param path 图片路径 
  91.      * @return 
  92.      */  
  93.     private Bitmap getBitmapFromFile(Activity mActivity,String imageName,String path){  
  94.         Bitmap bitmap = null;  
  95.         if(imageName != null){  
  96.             File file = null;  
  97.             String real_path = "";  
  98.             try {  
  99.                 if(Util.getInstance().hasSDCard()){  
  100.                     real_path = Util.getInstance().getExtPath() + (path != null && path.startsWith("/") ? path : "/" + path);  
  101.                 }else{  
  102.                     real_path = Util.getInstance().getPackagePath(mActivity) + (path != null && path.startsWith("/") ? path : "/" + path);  
  103.                 }  
  104.                 file = new File(real_path, imageName);  
  105.                 if(file.exists())  
  106.                 bitmap = BitmapFactory.decodeStream(new FileInputStream(file));  
  107.             } catch (Exception e) {  
  108.                 e.printStackTrace();  
  109.                 bitmap = null;  
  110.             }  
  111.         }  
  112.         return bitmap;  
  113.     }  
  114.       
  115.     /** 
  116.      * 将下载好的图片存放到文件中 
  117.      * @param path 图片路径 
  118.      * @param mActivity 
  119.      * @param imageName 图片名字 
  120.      * @param bitmap 图片 
  121.      * @return 
  122.      */  
  123.     private boolean setBitmapToFile(String path,Activity mActivity,String imageName,Bitmap bitmap){  
  124.         File file = null;  
  125.         String real_path = "";  
  126.         try {  
  127.             if(Util.getInstance().hasSDCard()){  
  128.                 real_path = Util.getInstance().getExtPath() + (path != null && path.startsWith("/") ? path : "/" + path);  
  129.             }else{  
  130.                 real_path = Util.getInstance().getPackagePath(mActivity) + (path != null && path.startsWith("/") ? path : "/" + path);  
  131.             }  
  132.             file = new File(real_path, imageName);  
  133.             if(!file.exists()){  
  134.                 File file2 = new File(real_path + "/");  
  135.                 file2.mkdirs();  
  136.             }  
  137.             file.createNewFile();  
  138.             FileOutputStream fos = null;  
  139.             if(Util.getInstance().hasSDCard()){  
  140.                 fos = new FileOutputStream(file);  
  141.             }else{  
  142.                 fos = mActivity.openFileOutput(imageName, Context.MODE_PRIVATE);  
  143.             }  
  144.               
  145.             if (imageName != null && (imageName.contains(".png") || imageName.contains(".PNG"))){  
  146.                 bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);  
  147.             }  
  148.             else{  
  149.                 bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos);  
  150.             }  
  151.             fos.flush();  
  152.             if(fos != null){  
  153.                 fos.close();  
  154.             }  
  155.             return true;  
  156.         } catch (Exception e) {  
  157.             e.printStackTrace();  
  158.             return false;  
  159.         }  
  160.     }  
  161.       
  162.     /** 
  163.      * 辅助方法,一般不调用 
  164.      * @param path 
  165.      * @param mActivity 
  166.      * @param imageName 
  167.      */  
  168.     private void removeBitmapFromFile(String path,Activity mActivity,String imageName){  
  169.         File file = null;  
  170.         String real_path = "";  
  171.         try {  
  172.             if(Util.getInstance().hasSDCard()){  
  173.                 real_path = Util.getInstance().getExtPath() + (path != null && path.startsWith("/") ? path : "/" + path);  
  174.             }else{  
  175.                 real_path = Util.getInstance().getPackagePath(mActivity) + (path != null && path.startsWith("/") ? path : "/" + path);  
  176.             }  
  177.             file = new File(real_path, imageName);  
  178.             if(file != null)  
  179.             file.delete();  
  180.         } catch (Exception e) {  
  181.             e.printStackTrace();  
  182.         }  
  183.     }  
  184.       
  185.     /** 
  186.      * 异步下载图片的方法 
  187.      * @author yanbin 
  188.      * 
  189.      */  
  190.     private class MyAsyncTask extends AsyncTask<String, Void, Bitmap>{  
  191.         private ImageView mImageView;  
  192.         private String url;  
  193.         private OnImageDownload download;  
  194.         private String path;  
  195.         private Activity mActivity;  
  196.           
  197.         public MyAsyncTask(String url,ImageView mImageView,String path,Activity mActivity,OnImageDownload download){  
  198.             this.mImageView = mImageView;  
  199.             this.url = url;  
  200.             this.path = path;  
  201.             this.mActivity = mActivity;  
  202.             this.download = download;  
  203.         }  
  204.   
  205.         @Override  
  206.         protected Bitmap doInBackground(String... params) {  
  207.             Bitmap data = null;  
  208.             if(url != null){  
  209.                 try {  
  210.                     URL c_url = new URL(url);  
  211.                     InputStream bitmap_data = c_url.openStream();  
  212.                     data = BitmapFactory.decodeStream(bitmap_data);  
  213.                     String imageName = Util.getInstance().getImageName(url);  
  214.                     if(!setBitmapToFile(path,mActivity,imageName, data)){  
  215.                         removeBitmapFromFile(path,mActivity,imageName);  
  216.                     }  
  217.                     imageCaches.put(url, new SoftReference<Bitmap>(data.createScaledBitmap(data, 100100true)));  
  218.                 } catch (Exception e) {  
  219.                     e.printStackTrace();  
  220.                 }  
  221.             }  
  222.             return data;  
  223.         }  
  224.   
  225.         @Override  
  226.         protected void onPreExecute() {  
  227.             super.onPreExecute();  
  228.         }  
  229.   
  230.         @Override  
  231.         protected void onPostExecute(Bitmap result) {  
  232.             //回调设置图片  
  233.             if(download != null){  
  234.                 download.onDownloadSucc(result,url,mImageView);  
  235.                 //该url对应的task已经下载完成,从map中将其删除  
  236.                 removeTaskFormMap(url);  
  237.             }  
  238.             super.onPostExecute(result);  
  239.         }  
  240.           
  241.     }  
  242. }  


Util.java类涉及到判断sdcard,获取图片存放路径以及从url中得到图片名称的操作,很简单,如下:

[java]  view plain copy
  1. public class Util {  
  2.     private static Util util;  
  3.     public static int flag = 0;  
  4.     private Util(){  
  5.           
  6.     }  
  7.       
  8.     public static Util getInstance(){  
  9.         if(util == null){  
  10.             util = new Util();  
  11.         }  
  12.         return util;  
  13.     }  
  14.       
  15.     /** 
  16.      * 判断是否有sdcard 
  17.      * @return 
  18.      */  
  19.     public boolean hasSDCard(){  
  20.         boolean b = false;  
  21.         if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){  
  22.             b = true;  
  23.         }  
  24.         return b;  
  25.     }  
  26.       
  27.     /** 
  28.      * 得到sdcard路径 
  29.      * @return 
  30.      */  
  31.     public String getExtPath(){  
  32.         String path = "";  
  33.         if(hasSDCard()){  
  34.             path = Environment.getExternalStorageDirectory().getPath();  
  35.         }  
  36.         return path;  
  37.     }  
  38.       
  39.     /** 
  40.      * 得到/data/data/yanbin.imagedownload目录 
  41.      * @param mActivity 
  42.      * @return 
  43.      */  
  44.     public String getPackagePath(Activity mActivity){  
  45.         return mActivity.getFilesDir().toString();  
  46.     }  
  47.   
  48.     /** 
  49.      * 根据url得到图片名 
  50.      * @param url 
  51.      * @return 
  52.      */  
  53.     public String getImageName(String url) {  
  54.         String imageName = "";  
  55.         if(url != null){  
  56.             imageName = url.substring(url.lastIndexOf("/") + 1);  
  57.         }  
  58.         return imageName;  
  59.     }  
  60. }  


至此,代码就全部贴完了。代码中我用了47张图片做测试,MyAsyncTask.java执行了47次,当最后listView中的最后一张图片展示出来的的时候,mapsize0。上面的一个程序主要解决了图片错位和AsyncTask重 复创建的问题。但是还是有不少需要完善的地方,比如同步,比如图片的定期清理(这个可以通过拿每张图片的最后更新时间,根据与当前时间的间隔将缓存图片删 除即可)。今天就到这里了,有更好的方法请推荐,有不懂的地方可以回复交流。自己动手丰衣足食,代码已经全部给出来了,希望童鞋们可以自己多写写,一起学 习。需要demo的就留下邮箱吧。