一、问题描述
android应用中经常涉及从网络中加载大量图片,为提升加载速度和效率,减少网络流量都会采用二级缓存和异步加载机制,所谓二级缓存就是通过先从内存中获取、再从文件中获取,最后才会访问网络。内存缓存(一级)本质上是map集合以key-value对的方式存储图片的url和bitmap信息,由于内存缓存会造成堆内存泄露, 管理相对复杂一些,可采用第三方组件,对于有经验的可自己编写组件,而文件缓存比较简单通常自己封装一下即可。下面就通过案例看如何实现网络图片加载的优化。
二、案例介绍
案例新闻的列表图片
三、主要核心组件
下面先看看实现一级缓存(内存)、二级缓存(磁盘文件)所编写的组件
1、memorycache
在内存中存储图片(一级缓存), 采用了1个map来缓存图片代码如下:
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
|
public class memorycache {
// 最大的缓存数
private static final int max_cache_capacity = 30 ;
//用map软引用的bitmap对象, 保证内存空间足够情况下不会被垃圾回收
private hashmap<string, softreference<bitmap>> mcachemap =
new linkedhashmap<string, softreference<bitmap>>() {
private static final long serialversionuid = 1l;
//当缓存数量超过规定大小(返回true)会清除最早放入缓存的
protected boolean removeeldestentry(
map.entry<string,softreference<bitmap>> eldest){
return size() > max_cache_capacity;};
};
/**
* 从缓存里取出图片
* @param id
* @return 如果缓存有,并且该图片没被释放,则返回该图片,否则返回null
*/
public bitmap get(string id){
if (!mcachemap.containskey(id)) return null ;
softreference<bitmap> ref = mcachemap.get(id);
return ref.get();
}
/**
* 将图片加入缓存
* @param id
* @param bitmap
*/
public void put(string id, bitmap bitmap){
mcachemap.put(id, new softreference<bitmap>(bitmap));
}
/**
* 清除所有缓存
*/
public void clear() {
try {
for (map.entry<string,softreference<bitmap>>entry :mcachemap.entryset())
{ softreference<bitmap> sr = entry.getvalue();
if ( null != sr) {
bitmap bmp = sr.get();
if ( null != bmp) bmp.recycle();
}
}
mcachemap.clear();
} catch (exception e) {
e.printstacktrace();}
}
}
|
2、filecache
在磁盘中缓存图片(二级缓存),代码如下
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
|
public class filecache {
//缓存文件目录
private file mcachedir;
/**
* 创建缓存文件目录,如果有sd卡,则使用sd,如果没有则使用系统自带缓存目录
* @param context
* @param cachedir 图片缓存的一级目录
*/
public filecache(context context, file cachedir, string dir){
if (android.os.environment.getexternalstoragestate().equals、(android.os.environment.media_mounted))
mcachedir = new file(cachedir, dir);
else
mcachedir = context.getcachedir(); // 如何获取系统内置的缓存存储路径
if (!mcachedir.exists()) mcachedir.mkdirs();
}
public file getfile(string url){
file f= null ;
try {
//对url进行编辑,解决中文路径问题
string filename = urlencoder.encode(url, "utf-8" );
f = new file(mcachedir, filename);
} catch (unsupportedencodingexception e) {
e.printstacktrace();
}
return f;
}
public void clear(){ //清除缓存文件
file[] files = mcachedir.listfiles();
for (file f:files)f.delete();
}
}
|
3、编写异步加载组件asyncimageloader
android中采用单线程模型即应用运行在ui主线程中,且android又是实时操作系统要求及时响应否则出现anr错误,因此对于耗时操作要求不能阻塞ui主线程,需要开启一个线程处理(如本应用中的图片加载)并将线程放入队列中,当运行完成后再通知ui主线程进行更改,同时移除任务——这就是异步任务,在android中实现异步可通过本系列一中所用到的asynctask或者使用thread+handler机制,在这里是完全是通过代码编写实现的,这样我们可以更清晰的看到异步通信的实现的本质,代码如下
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
|
public class asyncimageloader{
private memorycache mmemorycache; //内存缓存
private filecache mfilecache; //文件缓存
private executorservice mexecutorservice; //线程池
//记录已经加载图片的imageview
private map<imageview, string> mimageviews = collections
.synchronizedmap( new weakhashmap<imageview, string>());
//保存正在加载图片的url
private list<loadphototask> mtaskqueue = new arraylist<loadphototask>();
/**
* 默认采用一个大小为5的线程池
* @param context
* @param memorycache 所采用的高速缓存
* @param filecache 所采用的文件缓存
*/
public asyncimageloader(context context, memorycache memorycache, filecache filecache) {
mmemorycache = memorycache;
mfilecache = filecache;
mexecutorservice = executors.newfixedthreadpool( 5 ); //建立一个容量为5的固定尺寸的线程池(最大正在运行的线程数量)
}
/**
* 根据url加载相应的图片
* @param url
* @return 先从一级缓存中取图片有则直接返回,如果没有则异步从文件(二级缓存)中取,如果没有再从网络端获取
*/
public bitmap loadbitmap(imageview imageview, string url) {
//先将imageview记录到map中,表示该ui已经执行过图片加载了
mimageviews.put(imageview, url);
bitmap bitmap = mmemorycache.get(url); //先从一级缓存中获取图片
if (bitmap == null ) {
enquequeloadphoto(url, imageview); //再从二级缓存和网络中获取
}
return bitmap;
}
/**
* 加入图片下载队列
* @param url
*/
private void enquequeloadphoto(string url, imageview imageview) {
//如果任务已经存在,则不重新添加
if (istaskexisted(url))
return ;
loadphototask task = new loadphototask(url, imageview);
synchronized (mtaskqueue) {
mtaskqueue.add(task); //将任务添加到队列中
}
mexecutorservice.execute(task); //向线程池中提交任务,如果没有达到上限(5),则运行否则被阻塞
}
/**
* 判断下载队列中是否已经存在该任务
* @param url
* @return
*/
private boolean istaskexisted(string url) {
if (url == null )
return false ;
synchronized (mtaskqueue) {
int size = mtaskqueue.size();
for ( int i= 0 ; i<size; i++) {
loadphototask task = mtaskqueue.get(i);
if (task != null && task.geturl().equals(url))
return true ;
}
}
return false ;
}
/**
* 从缓存文件或者网络端获取图片
* @param url
*/
private bitmap getbitmapbyurl(string url) {
file f = mfilecache.getfile(url); //获得缓存图片路径
bitmap b = imageutil.decodefile(f); //获得文件的bitmap信息
if (b != null ) //不为空表示获得了缓存的文件
return b;
return imageutil.loadbitmapfromweb(url, f); //同网络获得图片
}
/**
* 判断该imageview是否已经加载过图片了(可用于判断是否需要进行加载图片)
* @param imageview
* @param url
* @return
*/
private boolean imageviewreused(imageview imageview, string url) {
string tag = mimageviews.get(imageview);
if (tag == null || !tag.equals(url))
return true ;
return false ;
}
private void removetask(loadphototask task) {
synchronized (mtaskqueue) {
mtaskqueue.remove(task);
}
}
class loadphototask implements runnable {
private string url;
private imageview imageview;
loadphototask(string url, imageview imageview) {
this .url = url;
this .imageview = imageview;
}
@override
public void run() {
if (imageviewreused(imageview, url)) { //判断imageview是否已经被复用
removetask( this ); //如果已经被复用则删除任务
return ;
}
bitmap bmp = getbitmapbyurl(url); //从缓存文件或者网络端获取图片
mmemorycache.put(url, bmp); // 将图片放入到一级缓存中
if (!imageviewreused(imageview, url)) { //若imageview未加图片则在ui线程中显示图片
bitmapdisplayer bd = new bitmapdisplayer(bmp, imageview, url); activity a = (activity) imageview.getcontext();
a.runonuithread(bd); //在ui线程调用bd组件的run方法,实现为imageview控件加载图片
}
removetask( this ); //从队列中移除任务
}
public string geturl() {
return url;
}
}
/**
*
*由ui线程中执行该组件的run方法
*/
class bitmapdisplayer implements runnable {
private bitmap bitmap;
private imageview imageview;
private string url;
public bitmapdisplayer(bitmap b, imageview imageview, string url) {
bitmap = b;
this .imageview = imageview;
this .url = url;
}
public void run() {
if (imageviewreused(imageview, url))
return ;
if (bitmap != null )
imageview.setimagebitmap(bitmap);
}
}
/**
* 释放资源
*/
public void destroy() {
mmemorycache.clear();
mmemorycache = null ;
mimageviews.clear();
mimageviews = null ;
mtaskqueue.clear();
mtaskqueue = null ;
mexecutorservice.shutdown();
mexecutorservice = null ;
}
}
|
编写完成之后,对于异步任务的执行只需调用asyncimageloader中的loadbitmap()方法即可非常方便,对于asyncimageloader组件的代码最好结合注释好好理解一下,这样对于android中线程之间的异步通信就会有深刻的认识。
4、工具类imageutil
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
|
public class imageutil {
/**
* 从网络获取图片,并缓存在指定的文件中
* @param url 图片url
* @param file 缓存文件
* @return
*/
public static bitmap loadbitmapfromweb(string url, file file) {
httpurlconnection conn = null ;
inputstream is = null ;
outputstream os = null ;
try {
bitmap bitmap = null ;
url imageurl = new url(url);
conn = (httpurlconnection) imageurl.openconnection();
conn.setconnecttimeout( 30000 );
conn.setreadtimeout( 30000 );
conn.setinstancefollowredirects( true );
is = conn.getinputstream();
os = new fileoutputstream(file);
copystream(is, os); //将图片缓存到磁盘中
bitmap = decodefile(file);
return bitmap;
} catch (exception ex) {
ex.printstacktrace();
return null ;
} finally {
try {
if (os != null ) os.close();
if (is != null ) is.close();
if (conn != null ) conn.disconnect();
} catch (ioexception e) { }
}
}
public static bitmap decodefile(file f) {
try {
return bitmapfactory.decodestream( new fileinputstream(f), null , null );
} catch (exception e) { }
return null ;
}
private 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) {
ex.printstacktrace();
}
}
}
|
四、测试应用
组件之间的时序图:
1、编写mainactivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class mainactivity extends activity {
listview list;
listviewadapter adapter;
@override
public void oncreate(bundle savedinstancestate) {
super .oncreate(savedinstancestate);
setcontentview(r.layout.main);
list=(listview)findviewbyid(r.id.list);
adapter= new listviewadapter( this , mstrings);
list.setadapter(adapter);
}
public void ondestroy(){
list.setadapter( null );
super .ondestroy();
adapter.destroy();
}
private string[] mstrings={
"http://news.jb51.net/userfiles/x_image/x_20150606083511_0.jpg" ,
"http://news.jb51.net/userfiles/x_image/x_20150606082847_0.jpg" ,
…..};
|
2、编写适配器
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
|
public class listviewadapter extends baseadapter {
private activity mactivity;
private string[] data;
private static layoutinflater inflater= null ;
private asyncimageloader imageloader; //异步组件
public listviewadapter(activity mactivity, string[] d) {
this .mactivity=mactivity;
data=d;
inflater = (layoutinflater)mactivity.getsystemservice(
context.layout_inflater_service);
memorycache mcache= new memorycache(); //内存缓存
file sdcard = android.os.environment.getexternalstoragedirectory(); //获得sd卡
file cachedir = new file(sdcard, "jereh_cache" ); //缓存根目录
filecache fcache= new filecache(mactivity, cachedir, "news_img" ); //文件缓存
imageloader = new asyncimageloader(mactivity, mcache,fcache);
}
public int getcount() {
return data.length;
}
public object getitem( int position) {
return position;
}
public long getitemid( int position) {
return position;
}
public view getview( int position, view convertview, viewgroup parent) {
viewholder vh= null ;
if (convertview== null ){
convertview = inflater.inflate(r.layout.item, null );
vh= new viewholder();
vh.tvtitle=(textview)convertview.findviewbyid(r.id.text);
vh.ivimg=(imageview)convertview.findviewbyid(r.id.image);
convertview.settag(vh);
} else {
vh=(viewholder)convertview.gettag();
}
vh.tvtitle.settext( "标题信息测试———— " +position);
vh.ivimg.settag(data[position]);
//异步加载图片,先从一级缓存、再二级缓存、最后网络获取图片
bitmap bmp = imageloader.loadbitmap(vh.ivimg, data[position]);
if (bmp == null ) {
vh.ivimg.setimageresource(r.drawable.default_big);
} else {
vh.ivimg.setimagebitmap(bmp);
}
return convertview;
}
private class viewholder{
textview tvtitle;
imageview ivimg;
}
public void destroy() {
imageloader.destroy();
}
}
|
想要了解更多内容的小伙伴,可以点击查看源码,亲自运行测试。