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

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

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

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

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

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

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

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

布局文件有两个,很简单,一个表示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.     <ListView  
  10.         android:layout_width="fill_parent"  
  11.         android:layout_height="wrap_content"  
  12.         android:cacheColorHint="@null"  
  13.         android:id="@+id/listview"  
  14.          />  

  15. </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. </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。性能考虑,我们使用convertView和ViewHolder来重用控件。这里涉及到比较关键的一步,我们会在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.             //图片地址就不贴了,自己去这篇帖子中找吧:
  9.             //其中有几张图片访问不了。  
  10.              };  

  11.     @Override  
  12.     public void onCreate(Bundle savedInstanceState) {  
  13.         super.onCreate(savedInstanceState);  
  14.         setContentView(R.layout.main);  
  15.         Util.flag = 0;  
  16.         mListView = (ListView) findViewById(R.id.listview);  
  17.         myListAdapter = new MyListAdapter();  
  18.         mListView.setAdapter(myListAdapter);  
  19.     }  

  20.     private class MyListAdapter extends BaseAdapter {  
  21.         private ViewHolder mHolder;  

  22.         @Override  
  23.         public int getCount() {  
  24.             return URLS.length;  
  25.         }  

  26.         @Override  
  27.         public Object getItem(int position) {  
  28.             return URLS[position];  
  29.         }  

  30.         @Override  
  31.         public long getItemId(int position) {  
  32.             return position;  
  33.         }  

  34.         @Override  
  35.         public View getView(int position, View convertView, ViewGroup parent) {  
  36.             //只有当convertView不存在的时候才去inflate子元素  
  37.             if (convertView == null) {  
  38.                 convertView = getLayoutInflater().inflate(R.layout.single_data,  
  39.                         null);  
  40.                  mHolder = new ViewHolder();  
  41.                  mHolder.mImageView = (ImageView) convertView.findViewById(R.id.image_view);  
  42.                  mHolder.mTextView = (TextView) convertView.findViewById(R.id.text_view);  
  43.                  convertView.setTag(mHolder);  
  44.             }else {  
  45.              mHolder = (ViewHolder) convertView.getTag();  
  46.              }  
  47.             final String url = URLS[position];  
  48.              mHolder.mTextView.setText(url != null ? url.substring(url.lastIndexOf("/") + 1) : "");  
  49.              mHolder.mImageView.setTag(URLS[position]);  
  50.             if (mDownloader == null) {  
  51.                 mDownloader = new ImageDownloader();  
  52.             }  
  53.             //这句代码的作用是为了解决convertView被重用的时候,图片预设的问题  
  54.             mHolder.mImageView.setImageResource(R.drawable.ic_launcher);  
  55.             if (mDownloader != null) {  
  56.                 //异步下载图片  
  57.                 mDownloader.imageDownload(url, mHolder.mImageView, "/yanbin",MainActivity.this, new OnImageDownload() {  
  58.                             @Override  
  59.                             public void onDownloadSucc(Bitmap bitmap,  
  60.                                     String c_url,ImageView mimageView) {  
  61.                                 ImageView imageView = (ImageView) mListView.findViewWithTag(c_url);  
  62.                                 if (imageView != null) {  
  63.                                     imageView.setImageBitmap(bitmap);  
  64.                                     imageView.setTag("");  
  65.                                 }   
  66.                             }  
  67.                         });  
  68.             }  
  69.             return convertView;  

  70.         }  

  71.         /** 
  72.          * 使用ViewHolder来优化listview 
  73.          * @author yanbin 
  74.          * 
  75.          */  
  76.         private class ViewHolder {  
  77.             ImageView mImageView;  
  78.             TextView mTextView;  
  79.         }  
  80.     }  
  81. }  


上面的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.      * 判断是否需要重新创建线程下载图片,如果需要,返回值为true。 
  46.      * @param url 
  47.      * @param mImageView 
  48.      * @return 
  49.      */  
  50.     private boolean needCreateNewTask(ImageView mImageView){  
  51.         boolean b = true;  
  52.         if(mImageView != null){  
  53.             String curr_task_url = (String)mImageView.getTag();  
  54.             if(isTasksContains(curr_task_url)){  
  55.                 b = false;  
  56.             }  
  57.         }  
  58.         return b;  
  59.     }  

  60.     /** 
  61.      * 检查该url(最终反映的是当前的ImageView的tag,tag会根据position的不同而不同)对应的task是否存在 
  62.      * @param url 
  63.      * @return 
  64.      */  
  65.     private boolean isTasksContains(String url){  
  66.         boolean b = false;  
  67.         if(map != null && map.get(url) != null){  
  68.             b = true;  
  69.         }  
  70.         return b;  
  71.     }  

  72.     /** 
  73.      * 删除map中该url的信息,这一步很重要,不然MyAsyncTask的引用会“一直”存在于map中 
  74.      * @param url 
  75.      */  
  76.     private void removeTaskFormMap(String url){  
  77.         if(url != null && map != null && map.get(url) != null){  
  78.             map.remove(url);  
  79.             System.out.println("当前map的大小=="+map.size());  
  80.         }  
  81.     }  

  82.     /** 
  83.      * 从文件中拿图片 
  84.      * @param mActivity  
  85.      * @param imageName 图片名字 
  86.      * @param path 图片路径 
  87.      * @return 
  88.      */  
  89.     private Bitmap getBitmapFromFile(Activity mActivity,String imageName,String path){  
  90.         Bitmap bitmap = null;  
  91.         if(imageName != null){  
  92.             File file = null;  
  93.             String real_path = "";  
  94.             try {  
  95.                 if(Util.getInstance().hasSDCard()){  
  96.                     real_path = Util.getInstance().getExtPath() + (path != null && path.startsWith("/") ? path : "/" + path);  
  97.                 }else{  
  98.                     real_path = Util.getInstance().getPackagePath(mActivity) + (path != null && path.startsWith("/") ? path : "/" + path);  
  99.                 }  
  100.                 file = new File(real_path, imageName);  
  101.                 if(file.exists())  
  102.                 bitmap = BitmapFactory.decodeStream(new FileInputStream(file));  
  103.             } catch (Exception e) {  
  104.                 e.printStackTrace();  
  105.                 bitmap = null;  
  106.             }  
  107.         }  
  108.         return bitmap;  
  109.     }  

  110.     /** 
  111.      * 将下载好的图片存放到文件中 
  112.      * @param path 图片路径 
  113.      * @param mActivity 
  114.      * @param imageName 图片名字 
  115.      * @param bitmap 图片 
  116.      * @return 
  117.      */  
  118.     private boolean setBitmapToFile(String path,Activity mActivity,String imageName,Bitmap bitmap){  
  119.         File file = null;  
  120.         String real_path = "";  
  121.         try {  
  122.             if(Util.getInstance().hasSDCard()){  
  123.                 real_path = Util.getInstance().getExtPath() + (path != null && path.startsWith("/") ? path : "/" + path);  
  124.             }else{  
  125.                 real_path = Util.getInstance().getPackagePath(mActivity) + (path != null && path.startsWith("/") ? path : "/" + path);  
  126.             }  
  127.             file = new File(real_path, imageName);  
  128.             if(!file.exists()){  
  129.                 File file2 = new File(real_path + "/");  
  130.                 file2.mkdirs();  
  131.             }  
  132.             file.createNewFile();  
  133.             FileOutputStream fos = null;  
  134.             if(Util.getInstance().hasSDCard()){  
  135.                 fos = new FileOutputStream(file);  
  136.             }else{  
  137.                 fos = mActivity.openFileOutput(imageName, Context.MODE_PRIVATE);  
  138.             }  

  139.             if (imageName != null && (imageName.contains(".png") || imageName.contains(".PNG"))){  
  140.                 bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos);  
  141.             }  
  142.             else{  
  143.                 bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos);  
  144.             }  
  145.             fos.flush();  
  146.             if(fos != null){  
  147.                 fos.close();  
  148.             }  
  149.             return true;  
  150.         } catch (Exception e) {  
  151.             e.printStackTrace();  
  152.             return false;  
  153.         }  
  154.     }  

  155.     /** 
  156.      * 辅助方法,一般不调用 
  157.      * @param path 
  158.      * @param mActivity 
  159.      * @param imageName 
  160.      */  
  161.     private void removeBitmapFromFile(String path,Activity mActivity,String imageName){  
  162.         File file = null;  
  163.         String real_path = "";  
  164.         try {  
  165.             if(Util.getInstance().hasSDCard()){  
  166.                 real_path = Util.getInstance().getExtPath() + (path != null && path.startsWith("/") ? path : "/" + path);  
  167.             }else{  
  168.                 real_path = Util.getInstance().getPackagePath(mActivity) + (path != null && path.startsWith("/") ? path : "/" + path);  
  169.             }  
  170.             file = new File(real_path, imageName);  
  171.             if(file != null)  
  172.             file.delete();  
  173.         } catch (Exception e) {  
  174.             e.printStackTrace();  
  175.         }  
  176.     }  

  177.     /** 
  178.      * 异步下载图片的方法 
  179.      * @author yanbin 
  180.      * 
  181.      */  
  182.     private class MyAsyncTask extends AsyncTask<String, Void, Bitmap>{  
  183.         private ImageView mImageView;  
  184.         private String url;  
  185.         private OnImageDownload download;  
  186.         private String path;  
  187.         private Activity mActivity;  

  188.         public MyAsyncTask(String url,ImageView mImageView,String path,Activity mActivity,OnImageDownload download){  
  189.             this.mImageView = mImageView;  
  190.             this.url = url;  
  191.             this.path = path;  
  192.             this.mActivity = mActivity;  
  193.             this.download = download;  
  194.         }  

  195.         @Override  
  196.         protected Bitmap doInBackground(String... params) {  
  197.             Bitmap data = null;  
  198.             if(url != null){  
  199.                 try {  
  200.                     URL c_url = new URL(url);  
  201.                     InputStream bitmap_data = c_url.openStream();  
  202.                     data = BitmapFactory.decodeStream(bitmap_data);  
  203.                     String imageName = Util.getInstance().getImageName(url);  
  204.                     if(!setBitmapToFile(path,mActivity,imageName, data)){  
  205.                         removeBitmapFromFile(path,mActivity,imageName);  
  206.                     }  
  207.                     imageCaches.put(url, new SoftReference<Bitmap>(data.createScaledBitmap(data, 100, 100, true)));  
  208.                 } catch (Exception e) {  
  209.                     e.printStackTrace();  
  210.                 }  
  211.             }  
  212.             return data;  
  213.         }  

  214.         @Override  
  215.         protected void onPreExecute() {  
  216.             super.onPreExecute();  
  217.         }  

  218.         @Override  
  219.         protected void onPostExecute(Bitmap result) {  
  220.             //回调设置图片  
  221.             if(download != null){  
  222.                 download.onDownloadSucc(result,url,mImageView);  
  223.                 //该url对应的task已经下载完成,从map中将其删除  
  224.                 removeTaskFormMap(url);  
  225.             }  
  226.             super.onPostExecute(result);  
  227.         }  

  228.     }  
  229. }  


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.     public static Util getInstance(){  
  7.         if(util == null){  
  8.             util = new Util();  
  9.         }  
  10.         return util;  
  11.     }  

  12.     /** 
  13.      * 判断是否有sdcard 
  14.      * @return 
  15.      */  
  16.     public boolean hasSDCard(){  
  17.         boolean b = false;  
  18.         if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){  
  19.             b = true;  
  20.         }  
  21.         return b;  
  22.     }  

  23.     /** 
  24.      * 得到sdcard路径 
  25.      * @return 
  26.      */  
  27.     public String getExtPath(){  
  28.         String path = "";  
  29.         if(hasSDCard()){  
  30.             path = Environment.getExternalStorageDirectory().getPath();  
  31.         }  
  32.         return path;  
  33.     }  

  34.     /** 
  35.      * 得到/data/data/yanbin.imagedownload目录 
  36.      * @param mActivity 
  37.      * @return 
  38.      */  
  39.     public String getPackagePath(Activity mActivity){  
  40.         return mActivity.getFilesDir().toString();  
  41.     }  

  42.     /** 
  43.      * 根据url得到图片名 
  44.      * @param url 
  45.      * @return 
  46.      */  
  47.     public String getImageName(String url) {  
  48.         String imageName = "";  
  49.         if(url != null){  
  50.             imageName = url.substring(url.lastIndexOf("/") + 1);  
  51.         }  
  52.         return imageName;  
  53.     }  
  54. }  


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