原创文章,转载请注明:转载自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利用率。
建立线程池的代码如下:
1
2
3
4
|
// 获取当前系统的CPU数目
int
cpuNums = Runtime.getRuntime().availableProcessors();
//根据系统资源情况灵活定义线程池大小
ExecutorService executorService = Executors.newFixedThreadPool(cpuNums +
1
);
|
从内存缓存读取图片是非常快的,如果内存缓存中有图片就可以直接获取,而不需要另起线程去异步加载,在内存缓存获取不到时才往线程池里添加新线程去加载图片。既然是异步的,那就要知道获取到的图片是要加载到哪个ImageView,可以将ImageView保存起来。另外,为了保证在整个应用中只有一个线程池,也不会出现多份缓存,图片加载的工具类最好用单例模式。ListView或GridView滑动时不加载图片,滑动停止后才加载图片,因此加一个是否允许加载图片的boolean变量。ListView或GridView初始化时是不滑动的,但也要加载图片,所以boolean值变量初始应该为true。
直接看图片加载的工具类ImageLoader的完整代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
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代码类似如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@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,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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、只加载当前屏幕