一、問題描述
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; //當緩存數量超過規定大?。ǚ祷豻rue)會清除最早放入緩存的 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(); } } |
想要了解更多內容的小伙伴,可以點擊查看源碼,親自運行測試。