ListView异步加载图片实现思路(优化篇)
程序员文章站
2023-12-12 19:19:28
在app应用中,listview的异步加载图片方式能够带来很好的用户体验,同时也是考量程序性能的一个重要指标。关于listview的异步加载,网上其实很多示例了,中心思想都...
在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里,这就是内存的二级缓存,有兴趣的童鞋不凡一试。
贴张效果图先:
异步加载图片基本思想:
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里,这就是内存的二级缓存,有兴趣的童鞋不凡一试。