Android ListView滑动过程中图片异步加载优化(配合Volley使用)

前言

今天带大家过一遍ListView常用的优化方案,重点在于解决ListView的item中包含异步加载图片时遇到的图片闪烁和显示错乱等情况.


ListView的item回收和重用

Android系统为了使得ListView性能优化,会为ListView增加item行缓存.简单来说,假设ListView有1万个item,但是Android只为其创建n个item(n为当前屏幕能显示的item的数量).ListView通过adapter的getView函数获取每行的item.滑动过程中:

  • 如果某行item已经划出屏幕,若该item在缓存内,则更新缓存内容.否则,将item通过put方法放入缓存中.
  • 获取划入屏幕的行item之前,会首先判断缓存中是否有可以的item.如果有,则作为convertView参数传递给adapter的getView函数.

了解了上面的大概,让我们从源码的角度来分析一下ListView的缓存机制.具体需要参考AbsListView的obtainView方法.

在这个方法中,有个特别重要的Field:

final RecycleBin mRecycler = new RecycleBin();

在obtainView方法中,我们就是通过mRecycler来获取缓存View的.

scrapView = mRecycler.getTransientStateView(position);
if (scrapView == null) {
    scrapView = mRecycler.getScrapView(position);
}

RecycleBin是一个内部类,这个类是帮我们复用item的关键.RecycleBin将每行item分为两类: ActiveViews和ScrapViews.其中ActiveViews是一开始显示在屏幕上的View,ScrapViews是潜在的可以让adapter使用的old views,也就是缓存view.
ActiveViews和ScrapViews表明AbsListView缓存依赖于这两个数组,ActiveViews用于存储屏幕上当前显示的Item,ScrapViews用于存储从屏幕移除可能会被复用的Item.

我们先来看一下, ActiveViews是如何产生的.

ActiveViews

在RecycleBin类中,是通过fillActiveViews来创造ActiveViews的.源码如下:

void fillActiveViews(int childCount, int firstActivePosition) {
    if (mActiveViews.length < childCount) {
        mActiveViews = new View[childCount];
    }
    mFirstActivePosition = firstActivePosition;
    final View[] activeViews = mActiveViews;
    for (int i = 0; i < childCount; i ++) {
        View child = getChildAt(i);
        AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
        if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
            activeViews[i] = child;
        }
    }
}

在ListView.java中的layoutChildren方法中,我们可以看到fillActiveViews的调用:

public void layoutChildren() {
    int childCount = getChildCount();
    final int firstPosition = mFirstPosition;
    final RecycleBin recycleBin = mRecycler;
    if (dataChanged) {
        for (int i = 0; i < childCount; i ++) {
            recycleBin.addScrapView(getChildAt(i));
        }
    } else {
        recycleBin.fillActiveViews(childCount, firstPosition);
    }
}

可以看出,如果数据发生变化则把当前的item放入到ScrapViews中,否则把当前显示的item放入ActiveViews中.

ScrapViews

在上面的代码中,我们主要到,当dataChanged时候,ListView开始构建ScrapView.

ViewHolder

同时,为了进一步提高效率,我们可以使用自定义的ViewHolder对象来缓存item的view对象,节省每次去findViewById的时间.参考代码如下:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder viewHolder;
    if (convertView == null) {
        convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
        viewHolder = new ViewHolder();
        viewHolder.mImg = (ImageView) convertView.findViewById(R.id.id_img);
        viewHolder.mTitle = (TextView) convertView.findViewById(R.id.id_title);
        convertView.setTag(viewHolder);
    } else {
        viewHolder = (ViewHolder)conview.getTag();
    }

    viewHolder.mImg.setImageBitmap(mBitmap);
    viewHolder.mTitle.setText("111");
}

private static class ViewHolder {
    ImageView mImg;
    TextView mTitle;
}

ListView的item缓存和ViewHolder的控件缓存虽然提高了ListView的滑动性能,但是当item存在异步图片加载的情况时,同样也带来了很多问题.


ListView的图片异步加载问题

ListView中当item存在图片异步加载时,可能出现的问题包括:

  • 行item的图片显示错乱
  • 行item的图片显示闪烁

问题原因

虽然图片异步加载可能出现的问题很多,但是导致问题出现的原因是一样的.

我们假设当前手机一屏能显示10个item.此时,item1正在异步加载图片,这时用户滑动的手机屏幕,当item1消失,item11进入时,item11有可能复用item1的view.由于图片异步加载通常涉及到网络请求会比较慢,而item11又恰好复用了item1,此时item1的图片才刚刚进行了显示.然后item11又通过异步加载加载属于item11的图片,item11的图片再次显示遮盖住item1的图片,因此会出现图片的显示错乱和闪烁.

解决思路

通过上面的分析,我们知道图片错乱或者闪烁的原因都是由于图片异步加载太慢和异步加载过程中ListView的item复用导致的.因此我们的解决方案是:

  1. 提高图片的异步加载速度,增加内存和硬盘二级缓存,这里我推荐直接使用Volley的ImageLoader即可.
  2. 我们可以在getView函数中给每个ImageView增加一个tag来标识图片来源,这样异步加载完成后我们先比较图片来源和需要加载的图片是否一致,如果一致再显示,不一致则不做处理.

提高图片异步加载速度

首先,Volley已经为我们提供了图片的本地存储,我们只需要为其增加内存缓存即可,这里推荐使用LruCache作为内存缓存.

示例代码(BitmapCache.java):

public class BitmapCache implements ImageLoader.ImageCache {
    private LruCache<String, Bitmap> mCache;

    /**
     * 初始化BitampCache缓存类.
     * 默认使用当前进程内存空间的1/8
     */
    public BitmapCache() {
        this((int) (Runtime.getRuntime().maxMemory() / 8));
    }

    /**
     * 初始化BitampCache缓存类.
     *
     * @param maxSize 缓存类默认存储大小,单位为字节
     */
    public BitmapCache(int maxSize) {
        mCache = new LruCache<String, Bitmap>(maxSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight();
            }
        };
    }

    @Override
    public Bitmap getBitmap(String s) {
        return mCache.get(s);
    }

    @Override
    public void putBitmap(String s, Bitmap bitmap) {
        mCache.put(s, bitmap);
    }
}

使用BitmapCache构造Volley的ImageLoader:

ImageLoader this.mImageLoader = new ImageLoader( VolleyManager.getInstance(context).getRequestQueue(), new BitmapCache());

增加图片来源标识

我们使用Volley中提供的默认ImageListener是无法增加图片来源标识的.

首先,给出Volley默认的ImageListener构造源码:

public static ImageLoader.ImageListener getImageListener(final ImageView view, final int defaultImageResId, final int errorImageResId) {
    return new ImageLoader.ImageListener() {
        public void onErrorResponse(VolleyError error) {
            if(errorImageResId != 0) {
                view.setImageResource(errorImageResId);
            }

        }

        public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
            if(response.getBitmap() != null) {
                view.setImageBitmap(response.getBitmap());
            } else if(defaultImageResId != 0) {
                view.setImageResource(defaultImageResId);
            }

        }
    };
}

从源码可以看出,当我们通过ImageLoader.getImageListener方法获取ImageListener,ImageListener再为ImageView设置图片显示时并没有判断图片来源.

既然已经看到了源码,那我们只需要参考其实现,并为其增加判断图片来源的函数即可,这里我使用网络图片的url作为唯一标识:

private ImageLoader.ImageListener createOptimizeImageListener(
        final String imageUrl, final ImageView view, final int defaultImageResId,
        final int errorImageResId) {
    return new ImageLoader.ImageListener() {
        public void onErrorResponse(VolleyError error) {
            String imageUrlTag = (String) view.getTag();
            if (ObjectUtils.isEquals(imageUrl, imageUrlTag)) {
                if (errorImageResId != 0) {
                    view.setImageResource(errorImageResId);
                }
            }
        }

        public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
            String imageUrlTag = (String) view.getTag();
            if (ObjectUtils.isEquals(imageUrl, imageUrlTag)) {
                if (response.getBitmap() != null) {
                    view.setImageBitmap(response.getBitmap());
                } else if (defaultImageResId != 0) {
                    view.setImageResource(defaultImageResId);
                }
            }
        }
    };
}
发布了541 篇原创文章 · 获赞 1067 · 访问量 285万+

猜你喜欢

转载自blog.csdn.net/zinss26914/article/details/52669118