一、問題描述
android應(yīng)用中經(jīng)常涉及從網(wǎng)絡(luò)中加載大量圖片,為提升加載速度和效率,減少網(wǎng)絡(luò)流量都會(huì)采用二級(jí)緩存和異步加載機(jī)制,所謂二級(jí)緩存就是通過先從內(nèi)存中獲取、再從文件中獲取,最后才會(huì)訪問網(wǎng)絡(luò)。內(nèi)存緩存(一級(jí))本質(zhì)上是map集合以key-value對(duì)的方式存儲(chǔ)圖片的url和bitmap信息,由于內(nèi)存緩存會(huì)造成堆內(nèi)存泄露, 管理相對(duì)復(fù)雜一些,可采用第三方組件,對(duì)于有經(jīng)驗(yàn)的可自己編寫組件,而文件緩存比較簡(jiǎn)單通常自己封裝一下即可。下面就通過案例看如何實(shí)現(xiàn)網(wǎng)絡(luò)圖片加載的優(yōu)化。
二、案例介紹
案例新聞的列表圖片
三、主要核心組件
下面先看看實(shí)現(xiàn)一級(jí)緩存(內(nèi)存)、二級(jí)緩存(磁盤文件)所編寫的組件
1、memorycache
在內(nèi)存中存儲(chǔ)圖片(一級(jí)緩存), 采用了1個(gè)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 { // 最大的緩存數(shù) private static final int max_cache_capacity = 30 ; //用map軟引用的bitmap對(duì)象, 保證內(nèi)存空間足夠情況下不會(huì)被垃圾回收 private hashmap<string, softreference<bitmap>> mcachemap = new linkedhashmap<string, softreference<bitmap>>() { private static final long serialversionuid = 1l; //當(dāng)緩存數(shù)量超過規(guī)定大小(返回true)會(huì)清除最早放入緩存的 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
在磁盤中緩存圖片(二級(jí)緩存),代碼如下
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; /** * 創(chuàng)建緩存文件目錄,如果有sd卡,則使用sd,如果沒有則使用系統(tǒng)自帶緩存目錄 * @param context * @param cachedir 圖片緩存的一級(jí)目錄 */ 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(); // 如何獲取系統(tǒng)內(nèi)置的緩存存儲(chǔ)路徑 if (!mcachedir.exists()) mcachedir.mkdirs(); } public file getfile(string url){ file f= null ; try { //對(duì)url進(jìn)行編輯,解決中文路徑問題 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中采用單線程模型即應(yīng)用運(yùn)行在ui主線程中,且android又是實(shí)時(shí)操作系統(tǒng)要求及時(shí)響應(yīng)否則出現(xiàn)anr錯(cuò)誤,因此對(duì)于耗時(shí)操作要求不能阻塞ui主線程,需要開啟一個(gè)線程處理(如本應(yīng)用中的圖片加載)并將線程放入隊(duì)列中,當(dāng)運(yùn)行完成后再通知ui主線程進(jìn)行更改,同時(shí)移除任務(wù)——這就是異步任務(wù),在android中實(shí)現(xiàn)異步可通過本系列一中所用到的asynctask或者使用thread+handler機(jī)制,在這里是完全是通過代碼編寫實(shí)現(xiàn)的,這樣我們可以更清晰的看到異步通信的實(shí)現(xiàn)的本質(zhì),代碼如下
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; //內(nèi)存緩存 private filecache mfilecache; //文件緩存 private executorservice mexecutorservice; //線程池 //記錄已經(jīng)加載圖片的imageview private map<imageview, string> mimageviews = collections .synchronizedmap( new weakhashmap<imageview, string>()); //保存正在加載圖片的url private list<loadphototask> mtaskqueue = new arraylist<loadphototask>(); /** * 默認(rèn)采用一個(gè)大小為5的線程池 * @param context * @param memorycache 所采用的高速緩存 * @param filecache 所采用的文件緩存 */ public asyncimageloader(context context, memorycache memorycache, filecache filecache) { mmemorycache = memorycache; mfilecache = filecache; mexecutorservice = executors.newfixedthreadpool( 5 ); //建立一個(gè)容量為5的固定尺寸的線程池(最大正在運(yùn)行的線程數(shù)量) } /** * 根據(jù)url加載相應(yīng)的圖片 * @param url * @return 先從一級(jí)緩存中取圖片有則直接返回,如果沒有則異步從文件(二級(jí)緩存)中取,如果沒有再從網(wǎng)絡(luò)端獲取 */ public bitmap loadbitmap(imageview imageview, string url) { //先將imageview記錄到map中,表示該ui已經(jīng)執(zhí)行過圖片加載了 mimageviews.put(imageview, url); bitmap bitmap = mmemorycache.get(url); //先從一級(jí)緩存中獲取圖片 if (bitmap == null ) { enquequeloadphoto(url, imageview); //再從二級(jí)緩存和網(wǎng)絡(luò)中獲取 } return bitmap; } /** * 加入圖片下載隊(duì)列 * @param url */ private void enquequeloadphoto(string url, imageview imageview) { //如果任務(wù)已經(jīng)存在,則不重新添加 if (istaskexisted(url)) return ; loadphototask task = new loadphototask(url, imageview); synchronized (mtaskqueue) { mtaskqueue.add(task); //將任務(wù)添加到隊(duì)列中 } mexecutorservice.execute(task); //向線程池中提交任務(wù),如果沒有達(dá)到上限(5),則運(yùn)行否則被阻塞 } /** * 判斷下載隊(duì)列中是否已經(jīng)存在該任務(wù) * @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 ; } /** * 從緩存文件或者網(wǎng)絡(luò)端獲取圖片 * @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); //同網(wǎng)絡(luò)獲得圖片 } /** * 判斷該imageview是否已經(jīng)加載過圖片了(可用于判斷是否需要進(jìn)行加載圖片) * @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是否已經(jīng)被復(fù)用 removetask( this ); //如果已經(jīng)被復(fù)用則刪除任務(wù) return ; } bitmap bmp = getbitmapbyurl(url); //從緩存文件或者網(wǎng)絡(luò)端獲取圖片 mmemorycache.put(url, bmp); // 將圖片放入到一級(jí)緩存中 if (!imageviewreused(imageview, url)) { //若imageview未加圖片則在ui線程中顯示圖片 bitmapdisplayer bd = new bitmapdisplayer(bmp, imageview, url); activity a = (activity) imageview.getcontext(); a.runonuithread(bd); //在ui線程調(diào)用bd組件的run方法,實(shí)現(xiàn)為imageview控件加載圖片 } removetask( this ); //從隊(duì)列中移除任務(wù) } public string geturl() { return url; } } /** * *由ui線程中執(zhí)行該組件的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 ; } } |
編寫完成之后,對(duì)于異步任務(wù)的執(zhí)行只需調(diào)用asyncimageloader中的loadbitmap()方法即可非常方便,對(duì)于asyncimageloader組件的代碼最好結(jié)合注釋好好理解一下,這樣對(duì)于android中線程之間的異步通信就會(huì)有深刻的認(rèn)識(shí)。
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 { /** * 從網(wǎng)絡(luò)獲取圖片,并緩存在指定的文件中 * @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(); } } } |
四、測(cè)試應(yīng)用
組件之間的時(shí)序圖:
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(); //內(nèi)存緩存 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( "標(biāo)題信息測(cè)試———— " +position); vh.ivimg.settag(data[position]); //異步加載圖片,先從一級(jí)緩存、再二級(jí)緩存、最后網(wǎng)絡(luò)獲取圖片 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(); } } |
想要了解更多內(nèi)容的小伙伴,可以點(diǎn)擊查看源碼,親自運(yùn)行測(cè)試。