您的位置:首页 > 移动开发 > Android开发

Android中listview图片错乱问题及解决方案

2016-03-25 23:17 453 查看
解析一:

Android ListView滚动过程中图片显示重复、错乱、闪烁的原因及解决方法,顺带提及ListView的缓存机制。

1、原因分析

ListView item缓存机制
:为了使得性能更优,ListView会缓存行item(某行对应的View)。ListView通过adapter的getView函数获得每行的item。滑动过程中,

a. 如果某行item已经滑出屏幕,若该item不在缓存内,则put进缓存,否则更新缓存;

b. 获取滑入屏幕的行item之前会先判断缓存中是否有可用的item,如果有,做为convertView参数传递给adapter的getView。

更具体可见源代码ListView.obtainView

这样,如下的getView写法就可以充分利用缓存大大提升ListView的性能。即便上万个行item,最多inflate的次数为n,n为一屏最多显示ListView 行item的个数。

ListView Adapter getView写法

Java

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.list_item, null);
holder = new ViewHolder();
……
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
}

/**
* ViewHolder
*
* @author trinea@trinea.cn 2013-08-01
*/
private static class ViewHolder {

ImageView appIcon;
TextView appName;
TextView appInfo;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

@Override

public View
getView(int
position,
View convertView,
ViewGroup parent)
{
ViewHolder
holder;

if
(convertView
== null)
{
convertView
= inflater.inflate(R.layout.list_item,
null);

holder
= new
ViewHolder();
……

convertView.setTag(holder);
}
else {

holder
= (ViewHolder)convertView.getTag();
}

}

/**
* ViewHolder

*
* @author trinea@trinea.cn 2013-08-01

*/
private
static class
ViewHolder {

ImageView
appIcon;

TextView appName;
TextView appInfo;

}

这样提升了性能,但同时也会造成另外一些问题:

a. 行item图片显示重复

这个显示重复是指当前行item显示了之前某行item的图片。

比如ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第14行,且滑动过程中该图片加载结束,第2行已不在屏幕内,根据上面介绍的缓存原理,第2行的view可能被第14行复用,这样我们看到的就是第14行显示了本该属于第2行的图片,造成显示重复。

b. 行item图片显示错乱

这个显示错乱是指某行item显示了不属于该行item的图片。

比如ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中listView已经滑动到了第14行,第2行已不在屏幕内,根据上面介绍的缓存原理,第2行的view可能被第14行复用,第14行显示了第2行的View,这时之前的图片加载结束,就会显示在第14行,造成错乱。

c. 行item图片显示闪烁

上面b的情况,第14行图片又很快加载结束,所以我们看到第14行先显示了第2行的图片,立马又显示了自己的图片进行覆盖造成闪烁错乱。

2、解决方法

通过上面的分析我们知道了出现错乱的原因是异步加载及对象被复用造成的,如果每次getView能给对象一个标识,在异步加载完成时比较标识与当前行item的标识是否一致,一致则显示,否则不做处理即可。

下面以使用ImageCache为ListView提供图片获取缓存为例,ListView中强烈推荐使用ImageCache

首先在listview adapter的getView中添加

Java

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.list_item, null);
holder = new ViewHolder();
……
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}

……
// add tag for image, to compare it when image loaded finish
imageView.setTag(imageUrl);
// if not in cache, restore default
if (!Cache.ICON_CACHE.get(imageUrl, imageView)) {
imageView.setImageDrawable(null);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Override

public View
getView(int
position,
View convertView,
ViewGroup parent)
{
ViewHolder
holder;

if
(convertView
== null)
{
convertView
= inflater.inflate(R.layout.list_item,
null);

holder
= new
ViewHolder();
……

convertView.setTag(holder);
}
else {

holder
= (ViewHolder)convertView.getTag();
}

……

// add tag for image, to compare it when image loaded finish
imageView.setTag(imageUrl);

// if not in cache, restore default
if
(!Cache.ICON_CACHE.get(imageUrl,
imageView))
{

imageView.setImageDrawable(null);
}

}

其中setTag表示设置标识,方便下面进行标志比对

if (!Cache.ICON_CACHE.get(imageUrl, imageView))

1

if
(!Cache.ICON_CACHE.get(imageUrl,
imageView))

Cache.ICON_CACHE为ImageCache的实例,表示如果不在缓存内则设置drawable为null(当然你可以可以设置为你自己的默认资源),防止显示了之前某个行item的图片,解决了a.
行item图片显示重复问题。

在ImageCache的OnImageCallbackListener的onGetSuccess函数中添加

Java

public void onGetSuccess(String imageUrl, Drawable imageDrawable, View view, boolean isInCache) {
// can be another view child, like textView and so on
if (view != null && imageDrawable != null) {
ImageView imageView = (ImageView)view;
// add tag judge, avoid listView cache and so on
String imageUrlTag = (String)imageView.getTag();
if (ObjectUtils.isEquals(imageUrlTag, imageUrl)) {
imageView.setImageDrawable(imageDrawable);
}
}
};

1
2
3
4
5
6
7
8
9
10
11

public
void onGetSuccess(String
imageUrl,
Drawable imageDrawable,
View view,
boolean isInCache)
{

// can be another view child, like textView and so on
if
(view
!= null
&& imageDrawable
!= null)
{

ImageView imageView
= (ImageView)view;
// add tag judge, avoid listView cache and so on

String
imageUrlTag =
(String)imageView.getTag();
if
(ObjectUtils.isEquals(imageUrlTag,
imageUrl))
{

imageView.setImageDrawable(imageDrawable);
}

}
};

在上面用String imageUrlTag = (String)imageView.getTag();取得之前设置的tag,然后和当前的url进行比较,如果相等则显示,解决了b. 行item图片显示错乱,c. 行item图片显示错乱的两个问题。其中ObjectUtils可见

解析二:

当重用 convertView 时,最初一屏显示 7 条记录, getView 被调用 7 次,创建了 7 个 convertView.

当 Item1 划出屏幕, Item8 进入屏幕时,这时没有为 Item8 创建新的 view 实例, Item8 复用的是

Item1 的 view 如果没有异步不会有任何问题,虽然 Item8 和 Item1 指向的是同一个 view,但滑到

Item8 时刷上了 Item8 的数据,这时 Item1 的数据和 Item8 是一样的,因为它们指向的是同一块内存,

但 Item1 已滚出了屏幕你看不见。当 Item1 再次可见时这块 view 又涮上了 Item1 的数据。

但当有异步下载时就有问题了,假设 Item1 的图片下载的比较慢,Item8 的图片下载的比较快,你滚上去

使 Item8 可见,这时 Item8 先显示它自己下载的图片没错,但等到 Item1 的图片也下载完时你发现

Item8 的图片也变成了 Item1 的图片,因为它们复用的是同一个 view。 如果 Item1 的图片下载的比

Item8 的图片快, Item1 先刷上自己下载的图片,这时你滑下去,Item8 的图片还没下载完, Item8

会先显示 Item1 的图片,因为它们是同一快内存,当 Item8 自己的图片下载完后 Item8 的图片又刷成

了自己的,你再滑上去使 Item1 可见, Item1 的图片也会和 Item8 的图片是一样的,

因为它们指向的是同一块内存。

最简单的解决方法就是网上说的,给 ImageView 设置一个 tag, 并预设一个图片。

当 Item1 比 Item8 图片下载的快时, 你滚下去使 Item8 可见,这时 ImageView 的 tag 被设成了

Item8 的 URL, 当 Item1 下载完时,由于 Item1 不可见现在的 tag 是 Item8 的 URL,所以不满足条件,

虽然下载下来了但不会设置到 ImageView 上, tag 标识的永远是可见 view 中图片的 URL。

关键代码如下:

// 给 ImageView 设置一个 tag
holder.img.setTag(imgUrl);
// 预设一个图片
holder.img.setImageResource(R.drawable.ic_launcher);

// 通过 tag 来防止图片错位
if (imageView.getTag() != null && imageView.getTag().equals(imageUrl)) {
imageView.setImageBitmap(result);
}


我参考网上资料写了一个 listview 异步加载图片的 DEMO:

(1) AsyncTask 下载图片

(2) 实现内存、文件二级缓存

内存缓存使用 LruCache,文件缓存使用 DiskLruCache

/**
* 图片异步加载类
*
* @author Leslie.Fang
*
*/
public class AsyncImageLoader {
private Context context;
// 内存缓存默认 5M
static final int MEM_CACHE_DEFAULT_SIZE = 5 * 1024 * 1024;
// 文件缓存默认 10M
static final int DISK_CACHE_DEFAULT_SIZE = 10 * 1024 * 1024;
// 一级内存缓存基于 LruCache
private LruCache<String, Bitmap> memCache;
// 二级文件缓存基于 DiskLruCache
private DiskLruCache diskCache;

public AsyncImageLoader(Context context) {
this.context = context;
initMemCache();
initDiskLruCache();
}

/**
* 初始化内存缓存
*/
private void initMemCache() {
memCache = new LruCache<String, Bitmap>(MEM_CACHE_DEFAULT_SIZE) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount();
}
};
}

/**
* 初始化文件缓存
*/
private void initDiskLruCache() {
try {
File cacheDir = getDiskCacheDir(context, "bitmap");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
diskCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, DISK_CACHE_DEFAULT_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 从内存缓存中拿
*
* @param url
*/
public Bitmap getBitmapFromMem(String url) {
return memCache.get(url);
}

/**
* 加入到内存缓存中
*
* @param url
* @param bitmap
*/
public void putBitmapToMem(String url, Bitmap bitmap) {
memCache.put(url, bitmap);
}

/**
* 从文件缓存中拿
*
* @param url
*/
public Bitmap getBitmapFromDisk(String url) {
try {
String key = hashKeyForDisk(url);
DiskLruCache.Snapshot snapShot = diskCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
return bitmap;
}
} catch (IOException e) {
e.printStackTrace();
}

return null;
}

/**
* 从 url 加载图片
*
* @param imageView
* @param imageUrl
*/
public Bitmap loadImage(ImageView imageView, String imageUrl) {
// 先从内存中拿
Bitmap bitmap = getBitmapFromMem(imageUrl);

if (bitmap != null) {
Log.i("leslie", "image exists in memory");
return bitmap;
}

// 再从文件中找
bitmap = getBitmapFromDisk(imageUrl);
if (bitmap != null) {
Log.i("leslie", "image exists in file");
// 重新缓存到内存中
putBitmapToMem(imageUrl, bitmap);
return bitmap;
}

// 内存和文件中都没有再从网络下载
if (!TextUtils.isEmpty(imageUrl)) {
new ImageDownloadTask(imageView).execute(imageUrl);
}

return null;
}

class ImageDownloadTask extends AsyncTask<String, Integer, Bitmap> {
private String imageUrl;
private ImageView imageView;

public ImageDownloadTask(ImageView imageView) {
this.imageView = imageView;
}

@Override
protected Bitmap doInBackground(String... params) {
try {
imageUrl = params[0];
String key = hashKeyForDisk(imageUrl);
// 下载成功后直接将图片流写入文件缓存
DiskLruCache.Editor editor = diskCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(imageUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
diskCache.flush();

Bitmap bitmap = getBitmapFromDisk(imageUrl);
if (bitmap != null) {
// 将图片加入到内存缓存中
putBitmapToMem(imageUrl, bitmap);
}

return bitmap;
} catch (IOException e) {
e.printStackTrace();
}

return null;
}

@Override
protected void onPostExecute(Bitmap result) {
super.onPostExecute(result);
if (result != null) {
// 通过 tag 来防止图片错位
if (imageView.getTag() != null && imageView.getTag().equals(imageUrl)) {
imageView.setImageBitmap(result);
}
}
}

private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
out = new BufferedOutputStream(outputStream, 8 * 1024);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return false;
}
}

private File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}

private int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}

private String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}

private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
}

如果使用 Volley 就简单的多了

同一个 URL 请求的重复发送,退出 activity 后队列中请求的 cancel,(上面的 demo 没有处理这两种情况)

图片的缓存等 都不用自己处理了, Volley 都封装好了。

Volley ListView 异步加载图片 demo: https://github.com/lesliebeijing/VolleyListViewImageDemo.git
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: