面試記錄第十五節——(bitmap釋放、lru、三級快取、圖片壓縮)
一、recycle釋放記憶體問題
答:
在Android2.3.3(API 10)及之前的版本中,Bitmap物件與其畫素資料是分開儲存的,Bitmap物件儲存在Dalvik heap中,而Bitmap物件的畫素資料則儲存在Native Memory(本地記憶體)中或者說Derict Memory(直接記憶體)中,這使得儲存在Native Memory中的畫素資料的釋放是不可預知的,我們可以呼叫recycle()方法來對Native Memory中的畫素資料進行釋放,前提是你可以清楚的確定Bitmap已不再使用了,如果你呼叫了Bitmap物件recycle()之後再將Bitmap繪製出來,就會出現”Canvas: trying to use a recycled bitmap”錯誤,而在Android3.0(API 11)之後,Bitmap的畫素資料和Bitmap物件一起儲存在Dalvik heap中,
注意:一個圖片載入到記憶體裡,其實是有兩部分資料組成,一部分是圖片的相關描述資訊,另一部分就是最重要的畫素資訊(這部分是有byte陣列組成的),android系統為了提高對圖片的處理效率,對於圖片的處理都是呼叫了底層的功能(由C語言實現的),也就是說一個圖片載入到記憶體裡後是使用兩部分的記憶體區域,簡單的說:一部分是java可用的記憶體區,一部分是c可用的記憶體區,這兩個記憶體區域是不能相互直接使用的,這個bitmap物件是由java分配的,當然不用的時候系統會自動回收了,可是那個對應的C可用的記憶體區域jvm是不能直接回收的,這個只能呼叫底層的功能釋放。所以你要呼叫recycle方法來釋放那一部分記憶體。
- 原始碼
/**
* Free the native object associated with this bitmap, and clear the
* reference to the pixel data. This will not free the pixel data synchronously;
* it simply allows it to be garbage collected if there are no other references.
* The bitmap is marked as "dead", meaning it will throw an exception if
* getPixels() or setPixels() is called, and will draw nothing. This operation
* cannot be reversed, so it should only be called if you are sure there are no
* further uses for the bitmap. This is an advanced call, and normally need
* not be called, since the normal GC process will free up this memory when
* there are no more references to this bitmap.
*/
- 翻譯:
釋放bitmap記憶體的時候,它會釋放和這個bitmap有關的native記憶體,同時它會清理有關資料物件的引用,但是這裡處理資料物件的引用,並不是立即清理資料(他並不是呼叫玩recycle()方法,就直接清理這個記憶體,他只是給垃圾回收機制傳送一個指令,讓它在bitmap沒有物件引用的時候,來進行垃圾回收)。當呼叫recycle()方法之後,這個bitmap就會被表明為“死亡狀態”。這個時候你在呼叫bitmap其他相關的方法,例如果get畫素()或set畫素()就會丟擲一個異常。同時這個操作是不可逆的,所以一定百分之百確定這個bitmap在以後的場景下,不會被你的程式在使用到,再去呼叫recycle()方法。所以谷歌原始碼中建議我們,可以不用去主動呼叫recycle()方法,因為在沒有引用的情況下,我們的垃圾回收機制會主動的清理記憶體。
public void recycle() {
if (!mRecycled && mNativePtr != 0) {
if (nativeRecycle(mNativePtr)) {
// return value indicates whether native pixel object was actually recycled.
// false indicates that it is still in use at the native level and these
// objects should not be collected now. They will be collected later when the
// Bitmap itself is collected.
mBuffer = null;
mNinePatchChunk = null;
}
mRecycled = true;
}
}
通過看原始碼,我們會發現,這個方法首先將這個Bitmap的引用置為null,然後呼叫了nativeRecycle(mNativeBitMap)方法,這個方法很明顯是個JNI呼叫,會呼叫底層的c或者c++程式碼就可以做到對該記憶體的立即回收,而不需要等待那不確定啥時候會執行的GC來回收了。
二、lru是個什麼玩意
答:LruCache是android提供的一個快取工具類,其演算法是最近最少使用演算法。它把最近使用的物件用“強引用”儲存在LinkedHashMap中,並且把最近最少使用的物件在快取值達到預設定值之前就從記憶體中移除。其在API12被引進,低版本可以用support包中的類。
- 原始碼:如圖01,其實很簡單,LinkedHashMap就是一個map。
1、其中用到的資料物件是LinkedHashMap,所以不要把這個類想的多麼深不可測,還是資料結構 + 演算法。既然用到了這個map,自然就要有新增修改和刪除操作了,用到了最近最少使用演算法,自然就要用到優先順序了。
2、作為快取,肯定有一個快取的大小,這個大小是可以設定的(自定義sizeOf())。當你訪問了一個item(需要快取的物件),這個item應該被加入到記憶體中,然後移動到一個佇列的頂部,如此迴圈後這個佇列的頂部應該是最近訪問的item了,而隊尾部就是很久沒有訪問的item,這樣我們就應該對隊尾部的item優先進行回收操作。
3、因為用到了HashMap,那麼就有這個資料儲存物件的特點(KEY-VALUE),放入這個map的item應該會被強引用,要回收這個物件的時候是讓這個key為空,這樣就讓有向圖找不到對應的value,最終被GC。
4、快取的最大特點是不做重複的勞動,如果你之前已經快取過這個item了,當你再次想要快取這個item時,應該會先判斷是否已經快取好了,如果已經快取,那麼就不執行新增的操作。
5、我們應該能通過某個方法來清空快取,這個快取在app被退出後就自動清理,不會常駐記憶體。
6、sizeof()方法。這個方法預設返回的是你快取的item數目,如果你想要自定義size的大小,直接重寫這個方法,返回自定義的值即可。
三、問:如果快取滿了的話,什麼方法來管理我們佇列的移除最進最少使用的item和新增新的item,
答:trimToSize()方法,
- 原始碼翻譯:刪除最年長的條目,直到剩餘條目的總數達到或低於請求的大小。
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.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
//計算現在快取的大小,然後減掉多餘的,內部呼叫的是sizeOf()方法
size -= safeSizeOf(key, value);
evictionCount++;
}
//如果你想在在我們的快取中實現二級快取,可以實現此方法,原始碼中是空方法。
entryRemoved(true, key, value, null);
}
}
- 計算inSampleSize
四、縮圖問題
答:縮圖是跟我們的inSampleSize是分不開的,它主要是根據我們inSampleSize算出的值,來響應的儲存bitmap到記憶體當中。
問:這裡有一個重要的知識點:inJustDecodeBounds,他是什麼原理呢?
答:如果該 值設為true那麼將不返回實際的bitmap,也不給其分配記憶體空間,這樣就避免記憶體溢位了。但是允許我們查詢圖片的資訊這其中就包括圖片大小資訊
options.outHeight (圖片原始高度)和option.outWidth(圖片原始寬度))。Options中有個屬性inSampleSize。我們可以充分利用它,實現縮放。
注意
例如,inSampleSize = = 2,則取出的縮圖的寬和高都是原始圖片的1/2,圖片大小就為原始大小的1/4。對於任何值< = 1的同樣處置為1。那麼相應的方法也就出來了,通過設定 inJustDecodeBounds為true,獲取到outHeight(圖片原始高度)和 outWidth(圖片的原始寬度),然後計算一個inSampleSize(縮放值),然後就可以取圖片了,這裡要注意的是,inSampleSize 可能小於0,必須做判斷。
具體步驟
第一步:BitmapFactory.Option
設定 inJustDecodeBounds為true
第二步:BitmapFactory.decodeFile(path,option)方法
解碼圖片路徑為一個位圖。如果指定的檔名是空的,或者不能解碼到一個位圖,函式將返回null[空值]。
獲取到outHeight(圖片原始高度)和 outWidth(圖片的原始寬度)
第三步:計算縮放比例,也可以不計算,直接給它設定一個值。
options.inSampleSize = “你的縮放倍數”;
如果是2就是高度和寬度都是原始的一半。
第四步:設定options.inJustDecodeBounds = false;
重新讀出圖片
bitmap = BitmapFactory.decodeFile(path, options);
public Bitmap decodeBitmap()
{
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// 通過這個bitmap獲取圖片的寬和高
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options);
if (bitmap == null)
{
System.out.println("bitmap為空");
}
float realWidth = options.outWidth;
float realHeight = options.outHeight;
System.out.println("真實圖片高度:" + realHeight + "寬度:" + realWidth);
// 計算縮放比
int scale = (int) ((realHeight > realWidth ? realHeight : realWidth) / 100);
if (scale <= 0)
{
scale = 1;
}
options.inSampleSize = scale;
options.inJustDecodeBounds = false;
// 注意這次要把options.inJustDecodeBounds 設為 false,這次圖片是要讀取出來的。
bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options);
int w = bitmap.getWidth();
int h = bitmap.getHeight();
System.out.println("縮圖高度:" + h + "寬度:" + w);
return bitmap;
}
}
五、三級快取概念
答:網路、本地、記憶體三個層次。
例如你首次開啟載入一張圖片,首次肯定只能從網路獲取,當請求成功,我們會吧圖片儲存在本地、記憶體各一份。之後你再次請求同一個url,我們就不用從網路獲取了,我們只需要從本地或者記憶體獲取即可。
網路請求:速度最慢。
記憶體請求:速度最快,也是首先會選擇載入的。