您的位置:首页 > 其它

ListView异步加载图片实现思路(优化篇)

2015-08-21 10:45 399 查看
用到ListView去加载数据,加载图片和文字是比较常见的,文字还好,图片从网络请求加载速度比较慢,以下是一个我在项目中使用到的异步加载图片的解决方案

在应用开发中,经常用到ListView去加载数据,加载图片和文字是比较常见的,文字还好,图片从网络请求加载速度比较慢,所以需要把图片的加载放到另一个线程中去执行,执行完了再更新UI线程。以下列出一个我在项目中使用到的异步加载图片的解决方案,代码没有上全,给出核心部分。

大致思路是这样:

1.利用软引用来缓存图片Bitmap,用图片的URL作为缓存查找的Key;

2.设两级缓存,一级是SoftReference,二级是本地SD卡;

3.如果两级缓存都没取到图片,则从服务器获取,并加入缓存;

4.加载完后通过回调接口通知UI更新;

以下是异步加载的关键代码,其中一些工具类没有给出,自己实现就可以,比如HttpRequest是我自己写的一个类。

复制代码代码如下:

public class AsyncImageLoader {

//Cache for image(Type String is the URL of image,the second parameter is soft reference)

private HashMap<String, SoftReference<Bitmap>> imageCache = null;

private Activity context;

public AsyncImageLoader(Activity context){

this.context = context;

imageCache = new HashMap<String, SoftReference<Bitmap>>();

}

public Bitmap loadImage(final ImageView imageView,final String imageURL,final ImageCallBack imageCallBack){

//If the cache contains the reference of bitmap then return

if (imageCache.containsKey(imageURL)) {

SoftReference<Bitmap> bitmapReference = imageCache.get(imageURL);

Bitmap bitmap = bitmapReference.get();

if (bitmap != null) {

return bitmap;

}

}

//Second cache,search local SD card

else {

String fileName = StringUtil.namePicture(imageURL);//获取文件名

boolean isExist = SystemUtils.findPhotoFromSDCard(Constant.INFO_PATH, fileName);

if (isExist) {//是否在SD卡存在图片

Bitmap bitmap = SystemUtils.getPhotoFromSDCard(Constant.INFO_PATH, fileName);

return bitmap;

}

}

final Handler myHandler = new Handler(){

@Override

public void handleMessage(Message msg)

{

imageCallBack.setImage(imageView, (Bitmap)msg.obj);

}

};

//If the bitmap not exists in cache or SD card,then get it from net

new Thread(){

@Override

public void run() {

// TODO Auto-generated method stub

boolean isNetwork = SystemUtils.checkNetwork(context);

if (isNetwork) {

InputStream photoStream = HttpRequest.getImageStream(imageURL);//这里是我自己写的一个类,目的是通过URL地址从服务器获取图片输入流

Bitmap bitmap;

try {

bitmap = ImageTools.getResizeBitmap(photoStream, 128, 128);

if (bitmap != null) {

String fileName = StringUtil.namePicture(imageURL);

//Save image to SD card

SystemUtils.savePhotoToSDCard(bitmap, fileName, Constant.INFO_PATH);

//Put soft reference to cache

imageCache.put(imageURL, new SoftReference<Bitmap>(bitmap));

//Send message to update UI

Message message = myHandler.obtainMessage(0, bitmap);

myHandler.sendMessage(message);

}

} catch (Exception e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}.start();

return null;

}

/**

* Interface for load image

* @author Ryan

*

*/

public interface ImageCallBack{

//Set image for imageview through bitmap

public void setImage(ImageView imageView,Bitmap bitmap);

}

}

在ListView的adapter的getView方法中:

复制代码代码如下:

Bitmap bitmap1 = asyncImageLoader.loadImage(viewHolder.imageView1, url1, new ImageCallBack() {

@Override

public void setImage(ImageView imageView, Bitmap bitmap) {

// TODO Auto-generated method stub

imageView.setImageBitmap(bitmap);

}

});

if (bitmap1 != null) {

viewHolder.imageView1.setImageBitmap(bitmap1);

}else {

viewHolder.imageView1.setImageResource(R.drawable.image_bg);

}

其中asyncImageLoader是在adapter的构造方法中初始化的,形成一个缓存。通过这个机制就可以实现ListView的图片异步加载,在用户体验上比直接加载要感觉好很多,那样会造成界面卡顿。这里是加载一张图片的情况,如果ListView的item中的图片是不定的,有可能是一张、两张、三张,该用什么方式呢,大家可以思考一下,并可以一起讨论一下,包括实现ListView滚动时不加载数据也是优化ListView加载的必要步骤。

关于listview的异步加载,网上其实很多示例了,中心思想都差不多,不过很多版本或是有bug,或是有性能问题有待优化,下面就让在下阐述其原理以探索个中奥秘。

在APP应用中,listview的异步加载图片方式能够带来很好的用户体验,同时也是考量程序性能的一个重要指标。关于listview的异步加载,网上其实很多示例了,中心思想都差不多,不过很多版本或是有bug,或是有性能问题有待优化。有鉴于此,本人在网上找了个相对理想的版本并在此基础上进行改造,下面就让在下阐述其原理以探索个中奥秘,与诸君共赏…

异步加载图片基本思想:

1.先从内存缓存中获取图片显示(内存缓冲)

2.获取不到的话从SD卡里获取(SD卡缓冲)

3.都获取不到的话从网络下载图片并保存到SD卡同时加入内存并显示(视情况看是否要显示)

OK,先上adapter的代码:

复制代码代码如下:

public class LoaderAdapter extends BaseAdapter{

private static final String TAG = "LoaderAdapter";

private boolean mBusy = false;

public void setFlagBusy(boolean busy) {

this.mBusy = busy;

}

private ImageLoader mImageLoader;

private int mCount;

private Context mContext;

private String[] urlArrays;

public LoaderAdapter(int count, Context context, String []url) {

this.mCount = count;

this.mContext = context;

urlArrays = url;

mImageLoader = new ImageLoader(context);

}

public ImageLoader getImageLoader(){

return mImageLoader;

}

@Override

public int getCount() {

return mCount;

}

@Override

public Object getItem(int position) {

return position;

}

@Override

public long getItemId(int position) {

return position;

}

@Override

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

ViewHolder viewHolder = null;

if (convertView == null) {

convertView = LayoutInflater.from(mContext).inflate(

R.layout.list_item, null);

viewHolder = new ViewHolder();

viewHolder.mTextView = (TextView) convertView

.findViewById(R.id.tv_tips);

viewHolder.mImageView = (ImageView) convertView

.findViewById(R.id.iv_image);

convertView.setTag(viewHolder);

} else {

viewHolder = (ViewHolder) convertView.getTag();

}

String url = "";

url = urlArrays[position % urlArrays.length];

viewHolder.mImageView.setImageResource(R.drawable.ic_launcher);

if (!mBusy) {

mImageLoader.DisplayImage(url, viewHolder.mImageView, false);

viewHolder.mTextView.setText("--" + position

+ "--IDLE ||TOUCH_SCROLL");

} else {

mImageLoader.DisplayImage(url, viewHolder.mImageView, true);

viewHolder.mTextView.setText("--" + position + "--FLING");

}

return convertView;

}

static class ViewHolder {

TextView mTextView;

ImageView mImageView;

}

}

关键代码是ImageLoader的DisplayImage方法,再看ImageLoader的实现

复制代码代码如下:

public class ImageLoader {

private MemoryCache memoryCache = new MemoryCache();

private AbstractFileCache fileCache;

private Map<ImageView, String> imageViews = Collections

.synchronizedMap(new WeakHashMap<ImageView, String>());

// 线程池

private ExecutorService executorService;

public ImageLoader(Context context) {

fileCache = new FileCache(context);

executorService = Executors.newFixedThreadPool(5);

}

// 最主要的方法

public void DisplayImage(String url, ImageView imageView, boolean isLoadOnlyFromCache) {

imageViews.put(imageView, url);

// 先从内存缓存中查找

Bitmap bitmap = memoryCache.get(url);

if (bitmap != null)

imageView.setImageBitmap(bitmap);

else if (!isLoadOnlyFromCache){

// 若没有的话则开启新线程加载图片

queuePhoto(url, imageView);

}

}

private void queuePhoto(String url, ImageView imageView) {

PhotoToLoad p = new PhotoToLoad(url, imageView);

executorService.submit(new PhotosLoader(p));

}

private Bitmap getBitmap(String url) {

File f = fileCache.getFile(url);

// 先从文件缓存中查找是否有

Bitmap b = null;

if (f != null && f.exists()){

b = decodeFile(f);

}

if (b != null){

return b;

}

// 最后从指定的url中下载图片

try {

Bitmap bitmap = null;

URL imageUrl = new URL(url);

HttpURLConnection conn = (HttpURLConnection) imageUrl

.openConnection();

conn.setConnectTimeout(30000);

conn.setReadTimeout(30000);

conn.setInstanceFollowRedirects(true);

InputStream is = conn.getInputStream();

OutputStream os = new FileOutputStream(f);

CopyStream(is, os);

os.close();

bitmap = decodeFile(f);

return bitmap;

} catch (Exception ex) {

Log.e("", "getBitmap catch Exception...\nmessage = " + ex.getMessage());

return null;

}

}

// decode这个图片并且按比例缩放以减少内存消耗,虚拟机对每张图片的缓存大小也是有限制的

private Bitmap decodeFile(File f) {

try {

// decode image size

BitmapFactory.Options o = new BitmapFactory.Options();

o.inJustDecodeBounds = true;

BitmapFactory.decodeStream(new FileInputStream(f), null, o);

// Find the correct scale value. It should be the power of 2.

final int REQUIRED_SIZE = 100;

int width_tmp = o.outWidth, height_tmp = o.outHeight;

int scale = 1;

while (true) {

if (width_tmp / 2 < REQUIRED_SIZE

|| height_tmp / 2 < REQUIRED_SIZE)

break;

width_tmp /= 2;

height_tmp /= 2;

scale *= 2;

}

// decode with inSampleSize

BitmapFactory.Options o2 = new BitmapFactory.Options();

o2.inSampleSize = scale;

return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);

} catch (FileNotFoundException e) {

}

return null;

}

// Task for the queue

private class PhotoToLoad {

public String url;

public ImageView imageView;

public PhotoToLoad(String u, ImageView i) {

url = u;

imageView = i;

}

}

class PhotosLoader implements Runnable {

PhotoToLoad photoToLoad;

PhotosLoader(PhotoToLoad photoToLoad) {

this.photoToLoad = photoToLoad;

}

@Override

public void run() {

if (imageViewReused(photoToLoad))

return;

Bitmap bmp = getBitmap(photoToLoad.url);

memoryCache.put(photoToLoad.url, bmp);

if (imageViewReused(photoToLoad))

return;

BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);

// 更新的操作放在UI线程中

Activity a = (Activity) photoToLoad.imageView.getContext();

a.runOnUiThread(bd);

}

}

/**

* 防止图片错位

*

* @param photoToLoad

* @return

*/

boolean imageViewReused(PhotoToLoad photoToLoad) {

String tag = imageViews.get(photoToLoad.imageView);

if (tag == null || !tag.equals(photoToLoad.url))

return true;

return false;

}

// 用于在UI线程中更新界面

class BitmapDisplayer implements Runnable {

Bitmap bitmap;

PhotoToLoad photoToLoad;

public BitmapDisplayer(Bitmap b, PhotoToLoad p) {

bitmap = b;

photoToLoad = p;

}

public void run() {

if (imageViewReused(photoToLoad))

return;

if (bitmap != null)

photoToLoad.imageView.setImageBitmap(bitmap);

}

}

public void clearCache() {

memoryCache.clear();

fileCache.clear();

}

public static void CopyStream(InputStream is, OutputStream os) {

final int buffer_size = 1024;

try {

byte[] bytes = new byte[buffer_size];

for (;;) {

int count = is.read(bytes, 0, buffer_size);

if (count == -1)

break;

os.write(bytes, 0, count);

}

} catch (Exception ex) {

Log.e("", "CopyStream catch Exception...");

}

}

}

先从内存中加载,没有则开启线程从SD卡或网络中获取,这里注意从SD卡获取图片是放在子线程里执行的,否则快速滑屏的话会不够流畅,这是优化一。于此同时,在adapter里有个busy变量,表示listview是否处于滑动状态,如果是滑动状态则仅从内存中获取图片,没有的话无需再开启线程去外存或网络获取图片,这是优化二。ImageLoader里的线程使用了线程池,从而避免了过多线程频繁创建和销毁,有的童鞋每次总是new一个线程去执行这是非常不可取的,好一点的用的AsyncTask类,其实内部也是用到了线程池。在从网络获取图片时,先是将其保存到sd卡,然后再加载到内存,这么做的好处是在加载到内存时可以做个压缩处理,以减少图片所占内存,这是优化三。

而图片错位问题的本质源于我们的listview使用了缓存convertView,假设一种场景,一个listview一屏显示九个item,那么在拉出第十个item的时候,事实上该item是重复使用了第一个item,也就是说在第一个item从网络中下载图片并最终要显示的时候其实该item已经不在当前显示区域内了,此时显示的后果将是在可能在第十个item上输出图像,这就导致了图片错位的问题。所以解决之道在于可见则显示,不可见则不显示。在ImageLoader里有个imageViews的map对象,就是用于保存当前显示区域图像对应的url集,在显示前判断处理一下即可。

下面再说下内存缓冲机制,本例采用的是LRU算法,先看看MemoryCache的实现

复制代码代码如下:

public class MemoryCache {

private static final String TAG = "MemoryCache";

// 放入缓存时是个同步操作

// LinkedHashMap构造方法的最后一个参数true代表这个map里的元素将按照最近使用次数由少到多排列,即LRU

// 这样的好处是如果要将缓存中的元素替换,则先遍历出最近最少使用的元素来替换以提高效率

private Map<String, Bitmap> cache = Collections

.synchronizedMap(new LinkedHashMap<String, Bitmap>(10, 1.5f, true));

// 缓存中图片所占用的字节,初始0,将通过此变量严格控制缓存所占用的堆内存

private long size = 0;// current allocated size

// 缓存只能占用的最大堆内存

private long limit = 1000000;// max memory in bytes

public MemoryCache() {

// use 25% of available heap size

setLimit(Runtime.getRuntime().maxMemory() / 10);

}

public void setLimit(long new_limit) {

limit = new_limit;

Log.i(TAG, "MemoryCache will use up to " + limit / 1024. / 1024. + "MB");

}

public Bitmap get(String id) {

try {

if (!cache.containsKey(id))

return null;

return cache.get(id);

} catch (NullPointerException ex) {

return null;

}

}

public void put(String id, Bitmap bitmap) {

try {

if (cache.containsKey(id))

size -= getSizeInBytes(cache.get(id));

cache.put(id, bitmap);

size += getSizeInBytes(bitmap);

checkSize();

} catch (Throwable th) {

th.printStackTrace();

}

}

/**

* 严格控制堆内存,如果超过将首先替换最近最少使用的那个图片缓存

*

*/

private void checkSize() {

Log.i(TAG, "cache size=" + size + " length=" + cache.size());

if (size > limit) {

// 先遍历最近最少使用的元素

Iterator<Entry<String, Bitmap>> iter = cache.entrySet().iterator();

while (iter.hasNext()) {

Entry<String, Bitmap> entry = iter.next();

size -= getSizeInBytes(entry.getValue());

iter.remove();

if (size <= limit)

break;

}

Log.i(TAG, "Clean cache. New size " + cache.size());

}

}

public void clear() {

cache.clear();

}

/**

* 图片占用的内存

*

* <A href='\"http://www.eoeandroid.com/home.php?mod=space&uid=2768922\"' target='\"_blank\"'>@Param</A> bitmap

*

* @return

*/

long getSizeInBytes(Bitmap bitmap) {

if (bitmap == null)

return 0;

return bitmap.getRowBytes() * bitmap.getHeight();

}

}

首先限制内存图片缓冲的堆内存大小,每次有图片往缓存里加时判断是否超过限制大小,超过的话就从中取出最少使用的图片并将其移除,当然这里如果不采用这种方式,换做软引用也是可行的,二者目的皆是最大程度的利用已存在于内存中的图片缓存,避免重复制造垃圾增加GC负担,OOM溢出往往皆因内存瞬时大量增加而垃圾回收不及时造成的。只不过二者区别在于LinkedHashMap里的图片缓存在没有移除出去之前是不会被GC回收的,而SoftReference里的图片缓存在没有其他引用保存时随时都会被GC回收。所以在使用LinkedHashMap这种LRU算法缓存更有利于图片的有效命中,当然二者配合使用的话效果更佳,即从LinkedHashMap里移除出的缓存放到SoftReference里,这就是内存的二级缓存,有兴趣的童鞋不凡一试。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: