Android技术积累:图片异步加载
来源:互联网 发布:php面向对象是什么 编辑:程序博客网 时间:2024/06/05 10:14
转载自Keegan小钢
本文链接地址:http://keegan-lee.diandian.com/post/2012-12-14/40046198902
当在ListView或GridView中要加载很多图片时,很容易出现滑动时的卡顿现象,以及出现OOM导致FC(Force Close)。
会出现卡顿现象主要是因为加载数据慢,要等数据加载完才能显示出来。可以通过将数据分页显示,以及将耗时的图片加载用异步的方式和图片缓存,这样就可以解决卡顿的问题。
大部分开发者在ListView或GridView加载图片时,都会在getView方法里创建新的线程去异步加载图片。然而,当屏幕快速向下滑动时,每个划过的Item都会调用getView一次,即会创建出很多线程,同一时间存在的线程太多,内存不够用了,自然就会OOM了。要避免OOM,就得控制好线程的数量,所以加个线程池就非常有必要了。
另外,当向下快速滑动屏幕时,也没必要加载滑动过的所有图片,只要加载滑动停止后当前屏幕的就足够了。仔细观察像微博、facebook或其他优秀的app,滑动屏幕时未加载过的图片是不会被加载的,当滑动停止后,也只加载当前屏幕内的图片。
那么,接下来就讨论实现的问题了。首先,图片是需要缓存的,前一篇文章已经对图片缓存做了总结(Android技术积累:图片缓存管理),直接拿过来用就行。然后,线程池维护多少个线程比较合适呢?这个很难界定,线程太少CPU不能得到充分利用,线程太多会降低性能,也加大了OOM的风险。线程池的最佳大小取决于可用处理器的数目以及工作队列中的任务的性质。若在一个具有N个处理器的系统上只有一个工作队列,其中全部是计算性质的任务,在线程池具有N或N+1个线程时一般会获得最大的CPU利用率。
建立线程池的代码如下:
// 获取当前系统的CPU数目
int
cpuNums = Runtime.getRuntime().availableProcessors();
//根据系统资源情况灵活定义线程池大小
ExecutorService executorService = Executors.newFixedThreadPool(cpuNums +
1
);
从内存缓存读取图片是非常快的,如果内存缓存中有图片就可以直接获取,而不需要另起线程去异步加载,在内存缓存获取不到时才往线程池里添加新线程去加载图片。既然是异步的,那就要知道获取到的图片是要加载到哪个ImageView,可以将ImageView保存起来。另外,为了保证在整个应用中只有一个线程池,也不会出现多份缓存,图片加载的工具类最好用单例模式。ListView或GridView滑动时不加载图片,滑动停止后才加载图片,因此加一个是否允许加载图片的boolean变量。ListView或GridView初始化时是不滑动的,但也要加载图片,所以boolean值变量初始应该为true。
直接看图片加载的工具类ImageLoader的完整代码:
public
class
ImageLoader {
private
static
ImageLoader instance;
private
ExecutorService executorService;
//线程池
private
ImageMemoryCache memoryCache;
//内存缓存
private
ImageFileCache fileCache;
//文件缓存
private
Map<String, ImageView> taskMap;
//存放任务
private
boolean
allowLoad =
true
;
//是否允许加载图片
private
ImageLoader(Context context) {
// 获取当前系统的CPU数目
int
cpuNums = Runtime.getRuntime().availableProcessors();
//根据系统资源情况灵活定义线程池大小
this
.executorService = Executors.newFixedThreadPool(cpuNums +
1
);
this
.memoryCache =
new
ImageMemoryCache(context);
this
.fileCache =
new
ImageFileCache();
this
.taskMap =
new
HashMap<String, ImageView>();
}
/**
* 使用单例,保证整个应用中只有一个线程池和一份内存缓存和文件缓存
*/
public
static
ImageLoader getInstance(Context context) {
if
(instance ==
null
)
instance =
new
ImageLoader(context);
return
instance;
}
/**
* 恢复为初始可加载图片的状态
*/
public
void
restore() {
this
.allowLoad =
true
;
}
/**
* 锁住时不允许加载图片
*/
public
void
lock() {
this
.allowLoad =
false
;
}
/**
* 解锁时加载图片
*/
public
void
unlock() {
this
.allowLoad =
true
;
doTask();
}
/**
* 添加任务
*/
public
void
addTask(String url, ImageView img) {
//先从内存缓存中获取,取到直接加载
Bitmap bitmap = memoryCache.getBitmapFromCache(url);
if
(bitmap !=
null
) {
img.setImageBitmap(bitmap);
}
else
{
synchronized
(taskMap) {
/**
* 因为ListView或GridView的原理是用上面移出屏幕的item去填充下面新显示的item,
* 这里的img是item里的内容,所以这里的taskMap保存的始终是当前屏幕内的所有ImageView。
*/
img.setTag(url);
taskMap.put(Integer.toString(img.hashCode()), img);
}
if
(allowLoad) {
doTask();
}
}
}
/**
* 加载存放任务中的所有图片
*/
private
void
doTask() {
synchronized
(taskMap) {
Collection<ImageView> con = taskMap.values();
for
(ImageView i : con) {
if
(i !=
null
) {
if
(i.getTag() !=
null
) {
loadImage((String) i.getTag(), i);
}
}
}
taskMap.clear();
}
}
private
void
loadImage(String url, ImageView img) {
this
.executorService.submit(
new
TaskWithResult(
new
TaskHandler(url, img), url));
}
/*** 获得一个图片,从三个地方获取,首先是内存缓存,然后是文件缓存,最后从网络获取 ***/
private
Bitmap getBitmap(String url) {
// 从内存缓存中获取图片
Bitmap result = memoryCache.getBitmapFromCache(url);
if
(result ==
null
) {
// 文件缓存中获取
result = fileCache.getImage(url);
if
(result ==
null
) {
// 从网络获取
result = ImageGetFromHttp.downloadBitmap(url);
if
(result !=
null
) {
fileCache.saveBitmap(result, url);
memoryCache.addBitmapToCache(url, result);
}
}
else
{
// 添加到内存缓存
memoryCache.addBitmapToCache(url, result);
}
}
return
result;
}
/*** 子线程任务 ***/
private
class
TaskWithResult
implements
Callable<String> {
private
String url;
private
Handler handler;
public
TaskWithResult(Handler handler, String url) {
this
.url = url;
this
.handler = handler;
}
@Override
public
String call()
throws
Exception {
Message msg =
new
Message();
msg.obj = getBitmap(url);
if
(msg.obj !=
null
) {
handler.sendMessage(msg);
}
return
url;
}
}
/*** 完成消息 ***/
private
class
TaskHandler
extends
Handler {
String url;
ImageView img;
public
TaskHandler(String url, ImageView img) {
this
.url = url;
this
.img = img;
}
@Override
public
void
handleMessage(Message msg) {
/*** 查看ImageView需要显示的图片是否被改变 ***/
if
(img.getTag().equals(url)) {
if
(msg.obj !=
null
) {
Bitmap bitmap = (Bitmap) msg.obj;
img.setImageBitmap(bitmap);
}
}
}
}
}
有一点需要注意,要保证taskMap保存的始终只是当前屏幕内的所有ImageView,在ImageAdapter的getView方法里必须使用ViewHolder模式,这样才能保证item被重用时相应的ImageView也被重用。getView代码类似如下:
@Override
public
View getView(
int
position, View convertView, ViewGroup parent) {
ViewHolder holder;
if
(convertView ==
null
) {
convertView = mInflater.inflate(R.layout.list_item,
null
);
holder =
new
ViewHolder();
holder.text = (TextView) convertView.findViewById(R.id.text);
holder.image = (ImageView) convertView.findViewById(R.id.img);
convertView.setTag(holder);
}
else
{
holder = (ViewHolder) convertView.getTag();
}
ListItem item = mItems.get(position);
//ListView的Item
holder.text.setText(item.getText());
holder.image.setImageResource(R.drawable.default_img);
//设置默认图片
mImageLoader.addTask(item.getImgUrl(), holder.image);
//添加任务
return
convertView;
}
static
class
ViewHolder {
TextView text;
ImageView image;
}
ListView或GridView滑动时就需要锁住不允许加载图片,滑动停止后解锁加载图片。因此,给ListView或GridView添加一个OnScrollListener,代码如下:
mImageLoader = ImageLoader.getInstance(context);
mListView.setOnScrollListener(onScrollListener);
OnScrollListener onScrollListener =
new
OnScrollListener() {
@Override
public
void
onScrollStateChanged(AbsListView view,
int
scrollState) {
switch
(scrollState) {
case
OnScrollListener.SCROLL_STATE_FLING:
mImageLoader.lock();
break
;
case
OnScrollListener.SCROLL_STATE_IDLE:
mImageLoader.unlock();
break
;
case
OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
mImageLoader.lock();
break
;
default
:
break
;
}
}
@Override
public
void
onScroll(AbsListView view,
int
firstVisibleItem,
int
visibleItemCount,
int
totalItemCount) {
}
};
至此,所有关键代码就全都列出来了。
异步加载图片,关键就在于三点:1、缓存;2、线程池;3、只加载当前屏幕
- Android技术积累:图片异步加载
- Android技术积累:图片异步加载
- Android技术积累:图片异步加载
- Android技术积累:图片异步加载
- Android技术积累:图片异步加载
- Android技术积累:图片异步加载
- Android技术积累:图片异步加载
- Android技术积累:图片异步加载
- android异步加载图片
- android 异步加载图片
- android异步加载图片
- android异步加载图片
- Android 异步加载图片
- Android 异步加载图片
- Android图片异步加载
- Android图片异步加载
- Android图片异步加载
- Android异步加载图片
- Linux主要shell命令详解
- 用MyEclipse搭建SSH框架 Struts Spring Hibernate
- 汇编 BL BX
- input/output
- redis作者谈memcached
- Android技术积累:图片异步加载
- 讯飞语音,感受微信输入新体验
- 每个软件工程师都应该尝试的5件事情
- 你不知道的Eclipse用法:使用Javadoc导出项目的API文档
- 一种分析代金券使用分布情况的方法python实现版(上)
- 洞悉linux下的Netfilter&iptables:如何理解连接跟踪机制?
- Linux中记录终端(Terminal)输出到文本文件
- spring依赖注入之手工装配
- 从程序员到CTO的Java技术路线图