标签:线程池 异步 性能 listview imageview
在 APP应用中,listview的异步加载图片方式能够带来很好的用户体验,同时也是考量程序性能的一个重要指标。关于listview的异步加载,网上 其实很多示例了,中心思想都差不多,不过很多版本或是有bug,或是有性能问题有待优化。有鉴于此,本人在网上找了个相对理想的版本并在此基础上进行改 造,下面就让在下阐述其原理以探索个中奥秘
贴张效果图先:
异步加载图片基本思想:
1. 先从内存缓存中获取图片显示(内存缓冲)
2. 获取不到的话从SD卡里获取(SD卡缓冲)
3. 都获取不到的话从网络下载图片并保存到SD卡同时加入内存并显示(视情况看是否要显示)
OK,先上adapter的代码:
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
|
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的实现
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
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的实现
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
|
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.jcodecraeer.com/\"" 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里,这就是内存的二级缓存,有兴趣的童鞋不凡一试。
下面附上工程链接:
LazyLoaderDemo.zip(937.26 KB)
Listview异步加载图片之优化篇,布布扣,bubuko.com
标签:线程池 异步 性能 listview imageview
原文地址:http://blog.csdn.net/fngy123/article/details/37575209