編輯:關於Android編程
如上圖所示,目前App中UI界面經常會涉及到圖片,特別是像“今日關注”新聞這類app中,圖片運用的幾率十分頻繁。當手機上需要顯示大量圖片類似listView、gridView控件並且用戶會上下滑動,即將浏覽過的圖片又加載一遍,若是不停的進行網絡請求,很快就會OOM,這時三級緩存顯得尤為重要,適時地利用資源,進行圖片緩存,下面就用一個新聞組圖demo進行圖片緩存演示。
1.三級緩存的順序<喎?/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPjxiciAvPg0Ko6gxo6nE2rTmu7q05qO6ILHIyOfLtdDo0qq809TYzbzGrMqxo6zPtc2ztdrSu7K9sru74daxvdPN+MLnx+vH86OstvjKx8rXz8jV0rXa0ru8tru6tOYmbWRhc2g7xNq05ru6tOY8YnIgLz4NCqOoMqOpsb612Lu6tOajuiDI57n7xNq05ru6tObW0MO709CjrL7Nu+G007Xatv68tru6tOYmbWRhc2g7sb612Lu6s+WjqLy0c2S/qKOpPGJyIC8+DQqjqDOjqc34wue7urTmo7ogyOe5+7G+tdi7urTm1tDDu9PQo6y+zbvhtNPN+MLnu7q05tbQz8LU2M28xqyhozwvcD4NCjxwPjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160902/2016090209031358.png" title="\" />
2. 三級緩存級別總結
(1)內存緩存: 速度快, 優先讀取
(2)本地緩存: 速度其次, 內存沒有,讀本地
(3)網絡緩存: 速度最慢, 本地也沒有,才訪問網絡
關於這個三級緩存的實現,其實 Xutils開源項目中BitmapUtils已經替我們封裝好了,下面新建一個MyBitmapUtils,自己實現三級緩存。
1.網絡緩存(NetCacheUtils )
/** * 三個泛型意義: * 第一個泛型:doInBackground裡的參數類型 * 第二個泛型: onProgressUpdate裡的參數類型 * 第三個泛型: * onPostExecute裡的參數類型及doInBackground的返回類型 */ private class BitmapTask extends AsyncTask
這裡的邏輯就是 doInBackground方法 異步網絡請求圖片,onPostExecute方法將圖片加載呈現出來。而 onPreExecute 的作用是預加載,使用不常。至於onProgressUpdate 可顯示出請求圖片過程中的進度,這兩個並非核心方法。
(1)doInBackground : 核心方法,請求網絡。大家都知道請求網絡是一個耗時操作,需要在子線程中進行,這裡也確實如此,不過不需要我們再new 一個Thread ,查看源碼可知異步AsyncTask已經幫我們做到了。在這一步需要做的就是,獲得方法參數中的url,進行網絡請求,下載圖片獲得Bitmap.
(2)onPostExecute: 核心方法,圖片加載完成後,顯示在手機屏幕上。大家也了解子線程中無法做UI更新,需要使用消息機制,給handler發送消息,在主線程中更新UI。這裡異步也都替我們做好了,查看源碼可知UI更新是在異步中的hanler中進行。在這一步需要做的就是,將請求獲得的Bitmap呈現到屏幕上。(更規范的是,還要將獲得的Bitmap存儲到內存和本地中,方便下次使用時可拿取緩存,不需重復請求網絡!!!)
/** * 網絡緩存工具類 * * */ public class NetCacheUtils { LocalCacheUtils mLocalCacheUtils; MemoryCacheUtils mMemoryCacheUtils; public NetCacheUtils(LocalCacheUtils localCacheUtils, MemoryCacheUtils memoryCacheUtils) { mLocalCacheUtils = localCacheUtils; mMemoryCacheUtils = memoryCacheUtils; } public void getBitmapFromNet(ImageView ivPic, String url) { BitmapTask task = new BitmapTask(); task.execute(new Object[] { ivPic, url }); } class BitmapTask extends AsyncTask
2. 本地緩存(LocalCacheUtils )
/** * 本地緩存工具類 * * */ public class LocalCacheUtils { private static final String LOCAL_PATH = Environment .getExternalStorageDirectory().getAbsolutePath() + "/zhbj_cache"; /** * 從本地讀取圖片 * * @param url * @return */ public Bitmap getBitmapFromLocal(String url) { try { String fileName = MD5Encoder.encode(url); File file = new File(LOCAL_PATH, fileName); if (file.exists()) { // 圖片壓縮 BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2;// 表示壓縮比例,2表示寬高都壓縮為原來的二分之一, 面積為四分之一 options.inPreferredConfig = Config.RGB_565;// 設置bitmap的格式,565可以降低內存占用 Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream( file), null, options); return bitmap; } else { return null; } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 向本地存圖片 * * @param url * @param bitmap */ public void putBitmapToLocal(String url, Bitmap bitmap) { try { String fileName = MD5Encoder.encode(url); File file = new File(LOCAL_PATH, fileName); File parent = file.getParentFile(); // 創建父文件夾 if (!parent.exists()) { parent.mkdirs(); } bitmap.compress(CompressFormat.JPEG, 100, new FileOutputStream(file)); } catch (Exception e) { e.printStackTrace(); } } }
如上所示,這裡對於本地(sd卡)緩存的操作就兩個方法,比較單一,一個存儲緩存數據方法—putBitmapToLocal,一個拿取緩存數據方法—getBitmapToLocal
(1)putBitmapToLocal: 這裡我們將每個存儲圖片的文件名設為 圖片對應的url地址(MD5加密後的),判斷父文件是否存在,不存在則新建,存在則直接存儲進去。
(2)getBitmapToLocal: 先從方法參數中獲取到圖片對應的url,進行查找,若存在則將圖片的Bitmap返回回去(最好返回前先壓縮),不存在則返回null。
3. 內存緩存(LocalCacheUtils )重點!!!
3.1 HashMap版
/** * 內存緩存工具類 */ public class MemoryCacheUtils { HashMapmMemoryCache = new HashMap ; /** * 從內存讀取圖片 * * @param url * @return */ public Bitmap getBitmapFromMemory(String url) { Bitmap bitmap = mMemoryCache.get(url); return bitmap; } /** * 向內存存圖片 * * @param url * @param bitmap */ public void putBitmapToMemory(String url, Bitmap bitmap) { mMemoryCache.put(url, bitmap); } }
如上所示,這裡對於內存緩存的操作也是兩個方法,一個是設置內存緩存方法—putBitmapToLocal,一個是取內存緩存方法—getBitmapToLocal。用對象來存儲圖片,集合來存儲對象,集合都在內存裡面,所以決定用集合。
關於Android,集合就涉及到兩個,ArrayList用的多,但是取數據時必須要傳遞數組位置;但是Hashmap用的是鍵值對結構,只要有了key,就可以找到對應的value。(而我們這裡的key就是每張圖片對應的url,value就是每個圖片 Bitmap對象)
3.2 軟引用版
你說以上就是內存緩存的重點?絕對不可能,Bitmap對象雖存在於集合中,但我們每次都 new 一個新的Bitmap,如果有大量的圖片,集合內存根本不夠,很快就會OOM,也就是內存溢出。也許你的手機內存很大,但是不管安卓設備總內存有多大,它只給每個APP分配一定內存大小(16M),所以內存是非常有限的,而且在這裡 垃圾回收機制是不起作用的!
3.2.1 棧、堆、垃圾回收器
如上圖所示,內存緩存這裡涉及到棧和堆。java裡的棧一般存的是成員變量、方法聲明、引用之類的。堆裡存儲的是一個又一個的對象。(例如,new了一個 p,p存在棧裡,但是 person對象存儲在 堆中,p引用,指向一個person對象)。垃圾回收器會定時地從堆裡回收圾釋放內存。(例如上圖,只要棧與堆中的連接斷掉,堆中的對象就是垃圾,回收站可進行回收。所以說,垃圾回收器有個特點:只回收沒有引用的對象!)
再回到內存溢出上,我們
HashMap
集合中有許多個對象,都被集合引用!這個引用一直在!垃圾回收器並不會回收,所以會導致內存溢出。以上只是一方面,而且即使它會回收這些引用的集合,可它是隔一段時間才會回收,無法及時清理內存!
現在我們需要解決的是:能否在引用的情況下,垃圾回收器可以照樣回收?
3.2.2 內存緩存中的 引用級別
(1) 強引用 默認引用, 即使內存溢出,也不會回收
(2) 軟引用 SoftReference, 內存不夠時, 會考慮回收
(3) 弱引用 WeakReference 內存不夠時, 更會考慮回收
(4)虛引用 PhantomReference 內存不夠時, 最優先考慮回收!
像Person p = new Person();就屬於強引用。回收器斷然不會回收!而虛引用則太容易被回收,所以最常用的是軟引用 和 弱引用,在需求不強烈或內存實在是不夠的情況下,垃圾回收器才會回收引用的對象。我們主要回收的是Bitmap對象,對集合進行包裝,使用軟引用。
//用法舉例 Bitmap bitmap = new Bitmap(); SoftReferencesBitmap = new SoftReference (bitmap); Bitmap bitmap2 = sBitmap.get();
( 軟引用版):
/** * 內存緩存工具類 */ public class MemoryCacheUtils { HashMap> mMemoryCache = new HashMap > ; /** * 從內存讀取圖片 * * @param url * @return */ public Bitmap getBitmapFromMemory(String url) { SoftReference softBitmap = mMemoryCache.get(url); if(softReference != null){ Bitmap bitmap = softReference.get(); return bitmap; } return null; } /** * 向內存存圖片 * * @param url * @param bitmap */ public void putBitmapToMemory(String url, Bitmap bitmap) { SoftReference softBitmap = new SoftReference (bitmap); mMemoryCache.put(url, bitmap); } }
3.3 LruCache 版(重點!!!)
可是自從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。這麼說來即使內存很充分的情況下,也有優先回收弱引用和軟引用。
官方文檔的截圖:
https://developer.android.com/training/displaying-bitmaps/cache-bitmap.html官方鏈接
翻譯: 在過去,我們經常會使用一種非常流行的內存緩存技術的實現,即軟引用或弱引用 (SoftReference or WeakReference)。 但是現在已經不再推薦使用這種方式了,因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的對象, 這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的數據會存儲在本地的內存當中,因而無法用一種可預見的方式將其釋放, 這就有潛在的風險造成應用程序的內存溢出並崩潰。所以看到還有很多相關文章還在推薦用軟引用或弱引用 (SoftReference or WeakReference),就有點out了。
所以為了解決這個問題,google為我們推薦了LruCache類,這個類在 V4包 下,非常適合用來緩存圖片。
3.3.1 LruCache
Lru定義 :least recentlly used 最近最少使用的算法。(比如說,先後使用A、B、C、A、C、D對象,這時會回收的則是B對象。)
LruCache : 可以將最近最少使用的對象回收掉, 從而保證內存不會超出范圍!
3.3.2 分配空間
獲得分配給App最大的內存大小 —— 16M(16777216/1024)
long maxMemory = Runtime.getRuntime().maxMemory(); mMemoryCache = new LruCache((int) (maxMemory / 8))
但是在分配內存的過程中,切不可一次分配全部內存出去,畢竟這只是App的一部分模塊,其余部分還需要空間。(分配1/8 —— 2M)
3.3.2 重寫LruCache 的 sizeOf方法
這個方法要返回每個對象的大小。Lru要控制內存的總大小,所以它需要知道每個Bitmap有多大。所以需要重寫這個方法,讓開發者自己計算,返回大小。
protected int sizeOf(String key, Bitmap value) { // int byteCount = value.getByteCount(); int byteCount = value.getRowBytes() * value.getHeight();// 計算圖片大小:每行字節數*高度 return byteCount; }
( LruCache版):
private LruCachemMemoryCache; public MemoryCacheUtils() { long maxMemory = Runtime.getRuntime().maxMemory();// 獲取分配給app的內存大小 System.out.println("maxMemory:" + maxMemory); mMemoryCache = new LruCache ((int) (maxMemory / 8)) { // 返回每個對象的大小 @Override protected int sizeOf(String key, Bitmap value) { // int byteCount = value.getByteCount();//有版本兼容問題 int byteCount = value.getRowBytes() * value.getHeight();// 計算圖片大小:每行字節數*高度 return byteCount; } }; } /** * 寫緩存 */ public void setMemoryCache(String url, Bitmap bitmap) { mMemoryCache.put(url, bitmap); } /** * 讀緩存 */ public Bitmap getMemoryCache(String url) { return mMemoryCache.get(url); }
4. 工具類,將以上三級緩存封裝起來
/** * 自定義三級緩存圖片加載工具 */ public class MyBitmapUtils { private NetCacheUtils mNetCacheUtils; private LocalCacheUtils mLocalCacheUtils; private MemoryCacheUtils mMemoryCacheUtils; public MyBitmapUtils() { mMemoryCacheUtils = new MemoryCacheUtils(); mLocalCacheUtils = new LocalCacheUtils(); mNetCacheUtils = new NetCacheUtils(mLocalCacheUtils, mMemoryCacheUtils); } public void display(ImageView imageView, String url) { // 設置默認圖片 imageView.setImageResource(R.drawable.pic_item_list_default); // 優先從內存中加載圖片, 速度最快, 不浪費流量 Bitmap bitmap = mMemoryCacheUtils.getMemoryCache(url); if (bitmap != null) { imageView.setImageBitmap(bitmap); System.out.println("從內存加載圖片啦"); return; } // 其次從本地(sdcard)加載圖片, 速度快, 不浪費流量 bitmap = mLocalCacheUtils.getLocalCache(url); if (bitmap != null) { imageView.setImageBitmap(bitmap); System.out.println("從本地加載圖片啦"); // 寫內存緩存 mMemoryCacheUtils.setMemoryCache(url, bitmap); return; } // 最後從網絡下載圖片, 速度慢, 浪費流量 mNetCacheUtils.getBitmapFromNet(imageView, url); } }
以上,將工具類封裝號之後,我們可以不使用 Xutils裡的方法,使用我們自定義的MyBitmapUtils,以下代碼為調用過程。
class PhotoAdapter extends BaseAdapter { //private BitmapUtils mBitmapUtils; private MyBitmapUtils mBitmapUtils; public PhotoAdapter() { mBitmapUtils = new MyBitmapUtils(); //mBitmapUtils = new BitmapUtils(mActivity); // mBitmapUtils // .configDefaultLoadingImage(R.drawable.pic_item_list_default); }
呈現出來的順序就是:
這是我測試之後的,如果是第一次打開這個模塊,最先使用的只能是網絡緩存,一旦第一次進行網絡緩存後,本地緩存和內存緩存就會有相應的數據。下一次再打開此模塊時,首先加載的是本地緩存,得到Bitmap**對象後,之後進行的都是 內存緩存**了。
Lru 就像我們家用的洗漱池裡小開口,水龍頭流出來的水就像是內存,所以我們的洗漱池會堵嗎?不會!如果你把口子給堵起來防水,很快水就會滿出來,就像是 內存溢出。這裡,我們來看下 V4 包下的 LruCache 源碼 。
public class LruCache{ private final LinkedHashMap map;
點進去一看,Lrucache是一個泛型,它維護了一個 LinkedHashMap,將來在存圖片的時候,底層也是存在一個HashMap裡。
1. LruCache 的 put 方法
public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; //!!!!!!! size += safeSizeOf(key, value); //!!!!!!! previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } //!!!!!!! trimToSize(maxSize); return previous; }
我們去找它的一個put 方法。
previous = map.put(key, value);標記感歎號地方 的 map 就是 一開始的 LinkedHashMap,底層就是對HashMap的封裝。
size += safeSizeOf(key, value);
全局維護了一個變量size,時時在統計集合目前對象大小。它走的就是sizeOf方法。但是源碼中方法返回的是1,
protected int sizeOf(K key, V value) { return 1; }
所以我們需要去重寫它的sizeOf方法。所以LruCache 在put 的時候都會把總大小計算出來,然後調用trimToSize(maxSize);方法,來看下此方法源碼
public void trimToSize(int maxSize) { while (true) { K key; V value; synchronized (this) { if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } //!!!!!!! if (size <= maxSize || map.isEmpty()) { break; } //!!!!!!! Map.EntrytoEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } //!!!!!!! entryRemoved(true, key, value, null); } }
一上來就是一個While循環,先不看拋出異常,直接看if判斷if (size <= maxSize || map.isEmpty()) { break; },如果內存正常,則break出去,否則
Map.EntrytoEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++;
通過map 拿到迭代器的第一個對象,再直接拿到key,再remove出去,所以總內存大小就減少了。這時繼續While循環,因為減少一個不一定符合大小,所以一直減少直到內存大小少於規定值為止!
所以LruCache所謂的算法:可以將最近最少使用的對象回收掉, 從而保證內存不會超出范圍。
其中的核心原理就在這裡,不停的刪掉開頭的key,這就是最近最少用的對象。
2. LruCache 的 get 方法
public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { mapValue = map.get(key); if (mapValue != null) { hitCount++; return mapValue; } missCount++; }
這裡的get方法則更簡單,參數將 key 傳過來,直接從map中get出對象,再return出來就行了。
3. LruCache 的 核心
最核心的地方其實就是維護一個 HashMap,再設置了一個全局變量 size來計算變量的總大小。一旦超出大小,就開始刪除對象,從而保證內存量在規定范圍內!
呼~這篇文章總算寫完了,拖了好多天,希望對你們有幫助 :)
回調介紹所謂回調,就是客戶程序Client調用服務程序Service中的某個方法A,然後Service又在某個時候反過來調用Client中的某個方法B,對於Client來
內存洩露,是Android開發者最頭疼的事。可能一處小小的內存洩露,都可能是毀於千裡之堤的蟻穴。 怎麼才能檢測內存洩露呢?網上教程非常多,不過很多都是使用Eclipse
Android客戶端請求服務器端的詳細解釋1. Android客戶端與服務器端通信方式:Android與服務器通信通常采用HTTP通信方式和Socket通信方式,而HTT
Starting創建手勢密碼可以查看 CreateGestureActivity.java 文件.登陸驗證手勢密碼可以看 GestureLoginActivity.jav