ListView异步加载图片--图片缓存和错位问题解决方案

时间:2021-05-20 20:15:35

问题1:
加载太多的图片很容易造成OOM异常。

一、图片缓存

方法1:使用二级缓存 ->自己维护一个缓存区
只要有足够的内存,就一直保持对象,直到发现内存吃紧且没有Strong Ref时才回收对象。
所以可以这么做:map里面的键是用来放图片地址的,既可以是网络上的图片地址,也可以SDcard上的图片地址,
map里面的值里面放的是持有软引用的Bitmap.

private Map<String, SoftReference<Bitmap>> imageMap = new HashMap<String, SoftReference<Bitmap>>();

每次为ImageView设置图片时,先去map中寻找,存在就直接用,不存在则从网络加载,下载好后存到map中。

方法2:使用一级缓存,LruCache
2.1 LRU算法简介:
LRU是Least Recently Used 近期最少使用算法。
内存管理的一种页面置换算法,对于在内存中但又不用的数据块(内存块)叫做LRU,操作系统会根据哪些数据属于LRU而将其移出内存而腾出空间来加载另外的数据。
什么是LRU算法? LRU是Least Recently Used的缩写,即最少使用页面置换算法,是为虚拟页式存储管理服务的。

2.2 在Android中,有一个叫做LruCache类专门用来做图片缓存处理的。
它有一个特点,当缓存的图片达到了预先设定的值的时候,那么近期使用次数最少的图片就会被回收掉。
步骤:1)要先设置缓存图片的内存大小,这里设置为手机内存的1/10
手机内存的获取方式:int MAXMEMONRY = (int) (Runtime.getRuntime() .maxMemory() / 10);
2)LruCache里面的键值对分别是URL和对应的图片
3)使用时和上面类似,每次给item中的ImageView设置图片时先从缓存中查找,存在直接用,不存在则从网络加载,加载完成存到缓存中。

private LruCache<String, BitmapDrawable> mMemoryCache;
public MyCache() {
// 获取应用程序最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 10;//大小为当前程序运行时内存的1/10
mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSize) {
@Override
protected int sizeOf(String key, BitmapDrawable drawable) {
return drawable.getBitmap().getByteCount();
}
};
}

方法3.使用三级缓存:利用外部存储(即文件系统来缓存下载的图片)
只是把缓存放到了外部存储中,其他用法不变。
注意:
3.1根据实际情况决定在程序结束后要不要把缓存文件删除,如果程序中加载的图片更新速度很快(如新闻,更新速度很快),就需要在程序结束后清空缓存文件。反之,可以不用删除。

方法4.一级三级缓存配合使用

设置图片时先从一级缓存中取,存在则直接使用,不存在再去三级缓存中查找,存在直接使用,不存在再从网络加载,
注意:
4.1加载完成后,分别存到一级缓存和二级缓存。使用这种方案时,可以把一级缓存设置小一点。
4.2根据实际情况决定在程序结束后要不要把缓存文件删除,如果程序中加载的图片更新速度很快(如新闻,更新速度很快),就需要在程序结束后清空缓存文件。反之,可以不用删除。

**

二、四种方案的比较

**
1.:方案1、2使用的内存是运行时内存,速度相对较快,方案3使用的是二级缓存,速度相对较慢,方案4是一级缓存和二级缓存的配合使用,速度适中。
2.:方案1是自己维护一个缓存,缓存中的图片何时被回收完全由GC决定,所以可能会占用很大内存;方案2是用Android提供的缓存,内部存储原理和方案1类似,但使用了LRU算法,优点能较智能的决定那些图片该回收,哪些不能回收,所以会把内存控制在一个较合理的状态。方案3使用二级缓存,速度虽然慢了点,但是外部存储基本不用担心内存不够用。方案4是一级缓存和二级缓存的结合使用,一级缓存加快使用速度,二级缓存增加使用内存。但操作较麻烦,保存需要两次,也浪费了一些内存。

总结:根据实际情况使用不同方案,方案1不推荐使用,方案2 3 4 各有优点.

**

三、listView异步加载图片错位的问题

**
1.问题:使用ListView异步加载图片时,如果不做处理,会出现图片图片错位的情况,特别是在网速不给力的情况下。
效果图:

ListView异步加载图片--图片缓存和错位问题解决方案

2.原因:由ListVIew内部回收机制所致。
RecycleBin机制:
ListView异步加载图片--图片缓存和错位问题解决方案

ListView异步加载图片--图片缓存和错位问题解决方案

分析:当重用 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 的图片是一样的,
因为它们指向的是同一块内存。

**

四、解决方案

**
1.给ImageView添加tag
1)在getView()方法中得到一个Imageview时,添加一个tag,tag一般是一个url
2)在异步任务加载图片成功时,通过listView的findViewWithTag(tag)方法得到一个imageview,然后设置图片


@Override
public View getView( int position, View convertView, ViewGroup parent) {
if (listView == null)
listView = (ListView) parent;
View view = null;
ImageView imageView = null;
if (convertView != null)
view = convertView;
else {
view = LayoutInflater.from(context).inflate(R.layout. image_item,
parent, false);
}
imageView = (ImageView) view.findViewById(R.id.image );
imageView.setImageResource(R.drawable. ic_launcher);
String url = data[position];
imageView.setTag(url);
BitmapDrawable bitmapDrawable = cache.getBitmapFromMemoryCache(url);
if (bitmapDrawable != null && imageView != null) {
imageView.setImageDrawable(bitmapDrawable);
} else {
new ImageLoadAsyncTask(listView , context , cache).execute(url);
}
return view;
}

@Override
protected void onPostExecute(BitmapDrawable drawable) {
ImageView mImageView = (ImageView) listView.findViewWithTag(imageUrl );
if (mImageView != null && drawable != null) {
mImageView.setImageDrawable(drawable);
}
}

2、使用Volley的NetworkImageView
1)在item布局文件中,用NetworkImageView替换ImageView
2)使用ImageLoader图片加载器

<?xml version= "1.0" encoding ="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width= "match_parent"
android:layout_height= "match_parent"
android:orientation= "vertical" >

<com.android.volley.toolbox.NetworkImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="120dp"
android:scaleType="fitXY"
android:src="@drawable/ic_launcher" />

</LinearLayout>
@Override
public View getView( int position, View convertView, ViewGroup parent) {
String url = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(
R.layout. image_item, n ull) ;
} else {
view = convertView;
}
NetworkImageView image = (NetworkImageView) view
.findViewById(R.id. image);
image.setDefaultImageResId(R.drawable. ic_launcher);
image.setErrorImageResId(R.drawable. ic_launcher);
image.setImageUrl(url, mImageLoader);
return view;
}

3、使用弱引用把asynctask和imageview相关联
1)本质是要让ImageView和 BitmapWorkerTask之间建立一个双向关联,互相持有对方的引用,再通过适当的逻辑判断来解决图片乱序问题,然后为了防止出现内存泄漏的情 况,双向关联要使用弱引用的方式建立。

1.自定义的一个 Drawable,让这个Drawable持有BitmapWorkerTask的弱引用。

/**
* 自定义的一个 Drawable,让这个 Drawable持有BitmapWorkerTask的弱引用。
*/

public class AsyncDrawable extends BitmapDrawable {
private WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(
bitmapWorkerTask);
}

public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference .get();
}
}

2.定义自己的异步加载任务,让这个任务拥有ImageView的弱引用

/**
* 异步下载图片的任务。
*
* @author guolin
*/

class BitmapWorkerTask extends AsyncTask<String, Void, BitmapDrawable> {

String imageUrl;

private BitmapCache bitmapCache;

private Context context;

private WeakReference<ImageView> imageViewReference;

public BitmapWorkerTask(ImageView imageView, Context context,
BitmapCache bitmapCache) {
imageViewReference = new WeakReference<ImageView>(imageView);
this.context = context;
this.bitmapCache = bitmapCache;
}

@Override
protected BitmapDrawable doInBackground(String... params) {
imageUrl = params[0];
// 在后台开始下载图片
Bitmap bitmap = downloadBitmap(imageUrl);
BitmapDrawable drawable = new BitmapDrawable(context.getResources(),
bitmap);
bitmapCache.addBitmapToMemoryCache(imageUrl, drawable);
return drawable;
}

@Override
protected void onPostExecute(BitmapDrawable drawable) {
ImageView imageView = getAttachedImageView();
if (imageView != null && drawable != null) {
imageView.setImageDrawable(drawable);
}
}

/**
* 获取当前BitmapWorkerTask所关联的ImageView。
*/

private ImageView getAttachedImageView() {
ImageView imageView = imageViewReference.get();
BitmapWorkerTask bitmapWorkerTask = BitmapWorkManager.getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask) {
return imageView;
}
return null;
}

/**
* 建立HTTP请求,并获取Bitmap对象。
*
* @param imageUrl
* 图片的URL地址
* @return 解析后的Bitmap对象
*/

private Bitmap downloadBitmap(String imageUrl) {
Bitmap bitmap = null;
HttpURLConnection con = null;
try {
URL url = new URL(imageUrl);
con = (HttpURLConnection) url.openConnection();
con.setConnectTimeout(5 * 1000);
con.setReadTimeout(10 * 1000);
bitmap = BitmapFactory.decodeStream(con.getInputStream());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (con != null) {
con.disconnect();
}
}
return bitmap;
}

}

3.使用

@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (mListView == null) {
mListView = (ListView) parent;
}
String url = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(
R.layout.image_item, null);
} else {
view = convertView;
}
ImageView image = (ImageView) view.findViewById(R.id.image);
BitmapDrawable drawable = mMemoryCache.getBitmapFromMemoryCache(url);// 从缓存中取对应url图片
if (drawable != null) {
image.setImageDrawable(drawable);
// cancelPotentialWork返回true代表当前ImageView正在加载另一张图片,
// 所以要cancel上个任务,来执行新的任务
} else if (BitmapWorkManager.cancelPotentialWork(url, image)) {
BitmapWorkerTask task = new BitmapWorkerTask(image, getContext(),
mMemoryCache);
AsyncDrawable asyncDrawable = new AsyncDrawable(getContext()
.getResources(), mLoadingBitmap, task);
image.setImageDrawable(asyncDrawable);
task.execute(url);
}
return view;
}

源码

注:Android中对于图片的加载有很多开源的框架,这里列举几个:
1.xUtils(中国人做的哦)
https://github.com/wyouflf/xUtils

2.Volley(谷歌,非常好用)
https://github.com/adamrocker/volley

3.Picasso(感觉没前两个好)
https://github.com/square/picasso

4.Glide
https://github.com/bumptech/glide

5.Universal ImageLoader

6.Fresco (Facebook,非常强大)
https://github.com/facebook/fresco