關於Bitmap的記憶體,載入和回收等
Bitmap載入圖片
Bitmap的載入離不開BitmapFactory類,關於Bitmap官方介紹:
Creates Bitmap objects from various sources, including files, streams, and byte-arrays.
BitmapFactory類提供了四類方法用來載入Bitmap:
- decodeFile(),從檔案系統載入。
- decodeResource(),資原始檔中載入。
- decodeStream(),從輸入流載入。
- decodeByteArray(),從位元組陣列中載入。
注意:檢視原始碼可以發現,decodeFile()
Bitmap的記憶體位置
在Android3.0之前:Bitmap的畫素資料存放在Native記憶體,而Bitmap物件本身則存放在Dalvik Heap中。
從Android3.0開始:Bitmap的記憶體就全部在Dalvik Heap裡了 。
Bitmap的記憶體回收
在Android3.0之前,需要使用Bitmap.recycle()進行Bitmap的記憶體回收。
從Android3.0開始,不需要手動回收Bitmap了。
Bitmap的記憶體複用
從Android3.0開始,在Bitmap中引入了一個新的欄位BitmapFactory.Options.inBitmap
Android4.4(API 19)之前只有格式為jpg、png,同等寬高(要求苛刻),inSampleSize為1的Bitmap才可以複用。
從Android4.4(API 19)開始被複用的Bitmap的記憶體大於需要新申請記憶體的Bitmap的記憶體就可以了。
使用快取
LruCache+DiskLruCache
出於對效能和app的考慮,我們肯定是想著第一次從網路中載入到圖片之後,能夠將圖片快取在記憶體和sd卡中,這樣,我們就不用頻繁的去網路中載入圖片,可以很好地控制記憶體問題。
一般都會考慮使用LruCache+DiskLruCache,LruCache作為Bitmap在記憶體中的存放容器,在sd卡則使用DiskLruCache來統一管理磁碟上的圖片快取。
SoftReference+inBitmap
之前提到,可以採用LruCache作為存放Bitmap的容器,而在LruCache中有一個方法值得留意,那就是entryRemoved(),按照文件給出的說法,在LruCache容器滿了需要淘汰存放其中的物件騰出空間的時候會呼叫此方法。
- 注意:這裡只是物件被淘汰出LruCache容器,但並不意味著物件的記憶體會立即被Dalvik虛擬機器回收掉。
此時可以在此方法中將Bitmap使用SoftReference包裹起來,並用事先準備好的一個HashSet容器來存放這些即將被回收的Bitmap,有人會問,這樣存放有什麼意義?
之前我們提到將inmutable設定為true,Bitmap的記憶體可以被複用,當然肯定要滿足之前所說的條件。
解碼方法對圖片進行decode的時候會檢查記憶體中是否有可複用的Bitmap,避免我們頻繁地去SD卡上載入圖片而造成系統性能的下降,畢竟從直接從記憶體中複用要比在SD卡上進行IO操作的效率要高很多。
Bitmap的畫素格式
1.ALPHA_8:顏色資訊只由透明度組成,佔8位。
2.ARGB_4444:顏色資訊由透明度與R(Red),G(Green),B(Blue)四部分組成,每個部分都佔4位,總共佔16位。
3.ARGB_8888:顏色資訊由透明度與R(Red),G(Green),B(Blue)四部分組成,每個部分都佔8位,總共佔32位。是Bitmap預設的顏色配置資訊,也是最佔空間的一種配置。
4.RGB_565:顏色資訊由R(Red),G(Green),B(Blue)三部分組成,R佔5位,G佔6位,B佔5位,總共佔16位。
通常我們優化Bitmap時,當需要做效能優化或者防止OOM,我們通常會使用RGB_565,因為ALPHA_8只有透明度,顯示一般圖片沒有意義,Bitmap.Config.ARGB_4444顯示圖片不清楚,Bitmap.Config.ARGB_8888佔用記憶體最多。
Bitmap的記憶體計算
Bitmap類中有一個方法getByteCount():
/**
* Returns the minimum number of bytes that can be used to store this bitmap's pixels.
*
* <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can
* no longer be used to determine memory usage of a bitmap. See {@link
* #getAllocationByteCount()}.</p>
*/
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
還有一個方法getAllocationByteCount():
/**
* Returns the size of the allocated memory used to store this bitmap's pixels.
*
* <p>This can be larger than the result of {@link #getByteCount()} if a bitmap is reused to
* decode other bitmaps of smaller size, or by manual reconfiguration. See {@link
* #reconfigure(int, int, Config)}, {@link #setWidth(int)}, {@link #setHeight(int)}, {@link
* #setConfig(Bitmap.Config)}, and {@link BitmapFactory.Options#inBitmap
* BitmapFactory.Options.inBitmap}. If a bitmap is not modified in this way, this value will be
* the same as that returned by {@link #getByteCount()}.</p>
*
* <p>This value will not change over the lifetime of a Bitmap.</p>
*
* @see #reconfigure(int, int, Config)
*/
public final int getAllocationByteCount() {
if (mBuffer == null) {
// native backed bitmaps don't support reconfiguration,
// so alloc size is always content size
return getByteCount();
}
return mBuffer.length;
}
通過方法註釋我們可以瞭解到,getByteCount()代表儲存Bitmap的色素需要的最少記憶體,而getAllocationByteCount()代表在記憶體中為Bitmap分配的記憶體大小。
其實getByteCount()方法是在API12加入的,代表儲存Bitmap的色素需要的最少記憶體。從API19開始getAllocationByteCount()方法代替了getByteCount()。
一般情況下getByteCount()和getAllocationByteCount()是相等的。但是Bitmap記憶體如果複用之後,兩者就不一樣了。
通過複用Bitmap來解碼圖片,如果被複用的Bitmap的記憶體比待分配記憶體的Bitmap大,那麼getByteCount()表示新解碼圖片佔用記憶體的大小(並非實際記憶體大小,實際大小是複用的那個Bitmap的大小),getAllocationByteCount()表示被複用Bitmap真實佔用的記憶體大小。
那getByteCount()和getAllocationByteCount()值是怎麼計算出來的呢?
例子
下面就來舉個例子計算理論上Bitmap載入一張圖片時,所佔記憶體的大小,和getByteCount()的結果比較一下。
假設
一張畫素為522*686的PNG圖片,把它放到drawable-xxhdpi目錄下,在三星s6上載入,getByteCount()的結果是2547360B。
推導
第一步
預設的畫素格式是ARGB_8888,之前已經說到了ARGB_8888格式下的一個畫素點佔用32位記憶體即4個位元組。
所以結果是:int res = 522*686*4
,1432368。
顯然和答案不一樣啊。
第二步
假設中說把圖片放到drawable-xxhdpi目錄下,在三星s6上載入。並不是隨口一說的,它們也是影響Bitmap所佔記憶體大小的重要因素。
我們讀取的是drawable目錄下面的圖片,用的是decodeResource方法,該方法本質上就兩步:
讀取原始資源,這個呼叫了Resource.openRawResource方法,這個方法呼叫完成之後會對TypedValue進行賦值,其中包含了原始資源的density等資訊。
呼叫decodeResourceStream對原始資源進行解碼和適配。這個過程實際上就是原始資源的density到螢幕density的一個對映。
原始資源的density其實取決於資源存放的目錄(比如xxhdpi對應的是480),而螢幕density的值是和裝置的硬體有關的,三星s6的值為640。載入時,原始的資源會自動進行縮放。
所以結果是:int res = (522 * 640 / 480) * (686 * 640 / 480) * 4
,2546432。
第三步
好像還是差那麼一點,其實系統是進行了精度處理。
所以最終結果是:int res = (522 * 640 / 480f + 0.5) * (686 * 640 / 480f + 0.5) * 4
,2547360。
inScaled
上面說的縮放和一個引數inScaled有關:
public static class Options {
/**
* Create a default Options object, which if left unchanged will give
* the same result from the decoder as if null were passed.
*/
public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
……
}
如果inScaled設定為true,就縮放。設定為false,則不進行縮放。預設的值從上面程式碼可以看到,就是true。
縮放的比例為inTargetDensity / inDensity。
但是縮放也只針對資原始檔有效,對於其他來源的圖片不起效果,我們可以從原始碼中引數上的解釋得知:
/**
* The pixel density to use for the bitmap. This will always result
* in the returned bitmap having a density set for it (see
* {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}). In addition,
* if {@link #inScaled} is set (which it is by default} and this
* density does not match {@link #inTargetDensity}, then the bitmap
* will be scaled to the target density before being returned.
*
* <p>If this is 0,
* {@link BitmapFactory#decodeResource(Resources, int)},
* {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
* and {@link BitmapFactory#decodeResourceStream}
* will fill in the density associated with the resource. The other
* functions will leave it as-is and no density will be applied.
*
* @see #inTargetDensity
* @see #inScreenDensity
* @see #inScaled
* @see Bitmap#setDensity(int)
* @see android.util.DisplayMetrics#densityDpi
*/
public int inDensity;
其中有一句The other functions will leave it as-is and no density will be applied
就是這個意思。
以上所說inDensity和inTargetDensity其實是DPI(dots per inch),關於DPI的概念請移步 全面理解Android中的Px,DPI,DIP,Density,Sp等概念。
結論
Bitmap載入資原始檔在記憶體當中佔用的大小取決於以下三點:
- 畫素格式,前面我們已經提到,如果是ARGB8888那麼就是一個畫素4個位元組,如果是RGB565那就是2個位元組。
- 原始檔案存放的資源目錄(是hdpi還是xxhdpi)
- 目標螢幕的DPI(同等條件下,紅米在資源方面消耗的記憶體肯定是要小於三星S6的)
Bitmap載入其他來源的圖片,就和畫素格式有關。
減少Bitmap記憶體佔用
合理選擇Bitmap的畫素格式
不需要透明度的情況下,我們通常使用RGB_565。
使用取樣
inSampleSize的值必須大於1時才會有效果,且取樣率同時作用於寬和高。當inSampleSize=1時,取樣後的圖片為圖片的原始大小。
當inSampleSize=n時,取樣後的圖片的寬高均為原始圖片寬高的1/n,這時畫素為原始圖片的1/(n*n),佔用記憶體也為原始圖片的1/(n*n)。
inSampleSize的取值應該總為2的整數倍,否則會向下取整,取一個最接近2的整數倍,比如inSampleSize=3時,系統會取inSampleSize=2。
假設一張1024*1024,模式為ARGB_8888的圖片,inSampleSize=2,原始佔用記憶體大小是4MB,取樣後的圖片佔用記憶體大小就是(1024/2) * (1024/2 )* 4 = 1MB。
- 圖片的質量壓縮:上述用inSampleSize壓縮是尺寸壓縮,Android中還有一種壓縮方式叫質量壓縮。質量壓縮是在保持畫素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的,經過它壓縮的圖片檔案大小(kb)會有改變,但是匯入成Bitmap後佔得記憶體是不變的,寬高也不會改變。因為要保持畫素不變,所以它就無法無限壓縮,到達一個值之後就不會繼續變小了。顯然這個方法並不適用與縮圖,其實也不適用於想通過壓縮圖片減少記憶體的適用,僅僅適用於想在保證圖片質量的同時減少檔案大小的情況而已。
使用矩陣
我們之前使用inSampleSize對圖片進行取樣,取樣之後記憶體是小了,可是圖的尺寸也小了,我們要用Canvas繪製原始大小的圖片該怎麼辦?就可以使用矩陣:
Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0, 0);
canvas.drawBitmap(bitmap, matrix, paint);
這樣,繪製出來的圖就是放大以後的效果了,不過佔用的記憶體卻仍然是我們取樣出來的大小。
如果我要把圖片放到ImageView當中呢?一樣可以,請看:
Matrix matrix = new Matrix();
matrix.postScale(2, 2, 0, 0);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);