ListView优化(1),列表错乱问题

这节我们来讨论一下ListView的优化问题,ListView是我们在开发中非常常用的控件之一,而在开发中也经常会遇到关于ListView的问题,下面我们主要就ListView的两个主要问题进行分析和解决。

  • ListView的列表错乱问题
  • ListView的卡顿问题

一、ListView的列表错乱问题

1. 问题描述

在我们使用ListView异步加载很多图片的时候,会发现有时应该出现图片A的地方出现了图片B,或者某人的头像变成了其他人的头像,这就是ListView的列表错乱问题。

ListView列表错乱原因有两种情况。

2. 问题原因

原因就在于我们在Adapter的getView方法中复用了convertView。原本复用convertView的出发点是好的,是为了避免重复加载列表布局,也就是说本来用于加载显示图片A的列表项布局(这里称为View1)在图片A的位置移除到屏幕外之后,View1会被复用(也就是convertView)并用于加载新的图片B,从而避免了对View1重复加载列表项布局。
而问题就在于,如果图片B加载失败,没有覆盖复用的View1之前显示的图片A,或者另一种情况,View1在加载图片A完成前,View1已经被复用并完成加载显示图片B,此时图片A才完成加载并显示在View1上。这两种情况都会造成原本应该显示图片B的控件View1显示了图片A。这就是所谓的ListView图片错乱。
总结两种情况就是:

  1. View1显示图片A,ListView滚动,View1被复用用于显示图片B,而图片B加载不成功,此时View1仍然显示的是图片A。
  2. View1正在加载图片A,ListView滚动,View1被复用用于显示图片B,图片B很快加载显示,然后图片A才加载完并显示在View1中,此时图片A会把图片B覆盖,导致View1显示的是图片A。

3. 问题代码

复用convertView的BaseAdapter的getView方法

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder vh = null;
    if(convertView == null)
    {
        convertView = LayoutInflater.from(mContext).inflate(R.layout.item_list, parent, false);
        vh = new ViewHolder(
                ((ImageView)convertView.findViewById(R.id.iv_uil)));
        convertView.setTag(vh);
        
    }else
    {
        vh = (ViewHolder) convertView.getTag();
    }
    
    //在子线程中异步加载图片,其中List包含图片的URL
    loadImageAync(list.get(position),vh.imageView);
    
    return convertView;
}

异步加载图片并在Handler中显示图片的代码:

public void loadImageAync(String uri, ImageView imageView)
{
    //利用线程池加载图片
    THREAD_POOL_EXECUTOR.execute(new Runnable()
        {
            @Override
            public void run()
            {
                //通过url获取Bitmap
                Bitmap bitmap = loadBitmap(uri);
                if(bitmap != null)
                {
                    //用一个封装类将uri,Bitmap和ImageView传送到Handler处理
                    LoaderResult result = new LoaderResult(iamgeView, uri, bitmap);
                    mMainHandler.obtainMessage(RESULT, reuslt).sendToTarget();
                }
            }
        });
}

//用于在主线程中加载图片的Handler
Handler mMainHandler = new Handler(Looper.getMainLooper())
{
    @Override
    public void handleMessage(Message msg)
    {
        LoaderResult result = (LoaderResult) msg.obj;
        ImageView imageView = result.getImageView();
        Bitmap bitmap = result.getBitmap();
        //显示图片到imageView中
        imageView.setImageBitmap(bitmap);
    }
};

上面这些就是我们通常用的,在子线程中加载图片,然后通过Handler将图片加载到ImageView当中。也就是这段代码,造成了图片错乱的原因。

4. 解决方法

解决切入点

回顾一下两种造成图片错乱的原因,一个是由于图片B加载不成功导致图片A没有被刷新,另一个是图片A加载时间长把已经加载完成的图片B覆盖了。两种原因不一样,因此要完全解决图片错乱的问题需要分别从两个问题原因入手。

第一种问题的解决方法

对于第一种情况,通常有这样的解决思路,那就是不管后面的图片B能不能加载成功,我在开始加载图片前我都对ImageView设置一个正在加载默认图片,这样即使后面的图片B加载失败,那显示的至少也是一个正在加载的图片(还可以在加载失败时显示失败图片),而不是图片A,这样就能避免了图片A显示在应该显示图片B的View上了。

这个方法能够解决第一种问题,通常这也就够了,因为大部分发生列表错乱的原因就是因为后面的图片加载不出来。

具体的实现就是,在getView中或者在loadImageAsync中异步加载图片前,给ImageView设置一张正在加载图片,更好的还需要在获取图片失败时将图片设置成一张失败图片,所以有两点需要改进。
第一种问题的解决方法:

public void loadImageAync(final String uri, final ImageView imageView)
{
    //1.在加载图片之前设置一个默认图片
    imageView.setImageDrawable(R.drawable.ic_loading);

    THREAD_POOL_EXECUTOR.execute(new Runnable()
        {
            @Override
            public void run()
            {
                Bitmap bitmap = loadBitmap(uri);
                //注意判断条件改变了
                if(bitmap == null)
                {
                    //2. 加载图片失败时设置一个失败图片
                    bitmap = getDefaultErrorBitmap();
                }

                LoaderResult result = new LoaderResult(iamgeView, uri, bitmap);
                mMainHandler.obtainMessage(RESULT, reuslt).sendToTarget();
            }
        });
}
第二种问题的解决方法

第二中问题就比较特殊了,根本原因就是,图片A不知道自己在绑定的ImageView中已经过期了,用来显示自己的ImageView已经被复用了,但是图片A还是不要脸的在加载完成之后不管不顾的显示到ImageView中,如果图片B加载的比A久,那么不会出现问题(会出现图片A闪一下然后立刻换成B),如果B加载得快,那么最后图片A就会把图片B给覆盖。

既然问题找出来了,那么解决方法就好想了,只要让ImageView知道图片A已经过期,不显示图片A就行了。

实现的方法就是,在加载图片A前,给ImageView设置一个标签,用于表示当前应该显示的图片A的uri。然后在图片A加载完成之后显示图片A前,再次验证ImageView的标签是否还和所加载的图片A的uri一致,这里标签会发生变化的原因就在于,ImageView被复用用于加载另一张图片B时,标签就会标称图片B的uri。如果最后uri一致,说明ImageView还没有被复用,然后便可以心安理得的显示图片A;如果不一致,说明ImageView已经被复用啦,已经不属于图片A的啦,此时就不要再显示A啦。

在代码中的实现主要分为两步,一是在加载图片之前为ImageView添加Tag,二是在Handler的handleMessage中显示图片前验证uri是否一致,一致的话才显示图片。以下是图片错乱的解决方法,包括了第一种问题的解决办法。

public void loadImageAync(final String uri, final ImageView imageView)
{
    imageView.setImageDrawable(R.drawable.ic_loading);

    //在加载之前为ImageView设置Tag为uri
    imageView.setTag(uri);

    THREAD_POOL_EXECUTOR.execute(new Runnable()
        {
            @Override
            public void run()
            {
                Bitmap bitmap = loadBitmap(uri);
                //注意判断条件改变了
                if(bitmap == null)
                {
                    bitmap = getDefaultErrorBitmap();
                }

                LoaderResult result = new LoaderResult(iamgeView, uri, bitmap);
                mMainHandler.obtainMessage(RESULT, reuslt).sendToTarget();
            }
        });
}

Handler mMainHandler = new Handler(Looper.getMainLooper())
{
    @Override
    public void handleMessage(Message msg)
    {
        LoaderResult result = (LoaderResult) msg.obj;
        ImageView imageView = result.getImageView();
        Bitmap bitmap = result.getBitmap();
        String uri = result.getUri();
        //如果uri一致才显示图片到imageView中
        if(uri.equals((String)imageView.getTag()))
        {
            imageView.setImageBitmap(bitmap);
        }else
        {
            Log.w(TAG,"set image bitmap, but uri has changed, ignored!");   
        }
    }
};

以上就是ListView列表错乱的产生原因以及解决方法啦~

你可能感兴趣的:(ListView优化(1),列表错乱问题)