在開發(fā)app時,顯示一張本地圖片,這張圖片在加載時會占用大多內(nèi)存呢?猜測占用內(nèi)存大小和以下幾個因素有關(guān):
- 設計師切圖,圖片本身的分辨率;
- 圖片所放文件夾代表的 密度 dpi;
- 手機自身的屏幕密度;
- 經(jīng)過系統(tǒng)縮放得到的最終加載到手機上圖片的密度和占用的內(nèi)存。
我們知道Android中在加載本地大圖時,很容易OOM,主要原因在于加載的Bitmap占用內(nèi)存太大。接下來將圍繞以下幾個問題說明如何計算一張Bitmap占用的內(nèi)存大小。
- 將一張分辨率為 720x1080 的圖片放到 xxhdpi 或者 hdpi ,同放在 xhdpi 標準文件夾下,對于同一臺手機占用內(nèi)存大小是否有變化?
- 同一張分辨率為 720x1080 的圖片被不同屏幕分辨率的手機加載,BitmapFactory 的成員變量 inDensity、 inScreenDensity、 inTargetDensity 會怎樣變化?這些值又是怎樣被賦值的,又是怎樣進行縮放的?
- 使用 decodeResource() 和 decodeStream() 有什么區(qū)別?
- Options 的 inDensity、 inTargetDensity 和 輸出的 Bitmap 的 mDensity 有什么關(guān)系?Bitmap 的 mWidth、 mHeight 與 Options 的 outputWidth、 outputHeight 有什么關(guān)系?
- 這些同計算 Bitmap 內(nèi)存占用大小的 長寬有什么關(guān)系?
在回答這些問題之前,先介紹一下DisplayMetrics和Bitmap及其相關(guān)類。
一、DisplayMetrics和Bitmap及其相關(guān)類
DisplayMetrics
說明:屏幕密度相關(guān)類,可以用于獲取屏幕高和寬以及屏幕密度density、每英寸點數(shù)densityDpi . 這里,density 數(shù)值為 1dp = density px;在 DisplayMetrics 中,這兩個是線性相關(guān):

Bitmap
說明:Bitmap 在 Android 中指的是一張圖片,可以是 png,也可以是 jpg等其他圖片格式。
作用:可以獲取圖像文件信息,對圖像進行剪切、旋轉(zhuǎn)、縮放、壓縮等操作,并可以指定格式保存圖像文件。
Bitmap.Config
說明:Bitmap 格式。除了尺寸外,影響一個圖片占用空間還有色彩細節(jié)。位圖位數(shù)越高表示可以存儲的顏色信息越多,圖像也就越清晰逼真。
- ALPHA_8:表示8位Alpha位圖,每像素占1byte內(nèi)存;
- RGB_565:表示R為5位,G為6位,B為5位,一共16位,每像素占2byte內(nèi)存;
- ARGB_4444:表示16位位圖,每像素占2byte內(nèi)存(poor quality - Android Deprecated);
- ARGB_8888:表示32位ARGB位圖,每像素占4byte內(nèi)存(Recommended)。
BitmapFactory
說明:提供解析Bitmap的靜態(tài)工廠方法。
BitmapFactory.Options
說明:用于解碼Bitmap時的各種參數(shù)控制。
幾個重要參數(shù):
inBitmap:在解析Bitmap時重用該Bitmap,但是必須相同大小的Bitmap & inMutable = true 才可重用;
inMutable :配置Bitmap是否可更改,如每隔幾個像素給Bmp添加一條直線;
inPreferredConfig:Config顏色位數(shù),默認值為Bitmap.Config.ARGB_888;
inDither:是否抖動,默認false(Android Depracated);
inPremultiplied:默認true,一般不改變其值。
inPurgeable:當存儲像素內(nèi)存空間 在系統(tǒng)內(nèi)存不足時 是否可被回收(Android L Deprecated);
inInputShareable:是否可以共享一個 InputStream (Android L Deprecated);
inPreferQualityOverSpeed:為true時會優(yōu)先保證 Bitmap 質(zhì)量,其次是解碼速度(Android N Deprecated);
inTempStorage:解碼時的臨時空間,建議 16K;
inJustDecodeBounds:為true時僅返回 Bitmap 寬高等屬性,返回bmp=null,為false時才返回占內(nèi)存的 bmp;
inSampleSize:表示 Bitmap 的壓縮比例,值必須 > 1 & 是2的冪次方。inSampleSize = 2 時,表示壓縮寬高各1/2,最后返回原始圖1/4大小的Bitmap;
inDensity:表示 Bitmap 像素密度;
inTargetDensity:表示 Bitmap 最終的像素密度;
inScreenDensity:表示當前屏幕的像素密度;
inScaled:默認為true,是否支持縮放,設置為true時,Bitmap將以 inTargetDensity 的值進行縮放;
outputWidth:返回的 Bitmap的寬;
outputHeight:返回的 Bitmap的高。
以一張類圖說明Bitmap、BitmapFactory和BitmapFactory.Options三者之間的關(guān)系,如下圖所示:

二、ImageView 設置圖片 & Bitmap創(chuàng)建流程
ImageView 設置圖片
一般地,給 ImageView 設置資源圖片時,會用到四種方式:setImageResource(), setImageUri(), setImageBitmap(), setImageDrawable。這四種方式有什么區(qū)別呢?用一張圖來展示:

總結(jié):由上可知,ImageView設置本地圖片會先生成 Bitmap 再將 Bitmap 轉(zhuǎn)成 Drawable,最終通過 setImageDrawable() 設置;
【所以這步是否可以看做使用 setImageDrawable 會跳過讀取和解碼 Bitmap 操作,為最優(yōu)設置本地圖片方式呢?
—— 需測試內(nèi)存占用情況方可驗證。】
Bitmap創(chuàng)建流程
BitmapFactory 提供了五種方式來創(chuàng)建Bitmap,分別是:decodeFile, decodeResource, decodeByteArray, decodeStream, decodeFileDescription,這里只介紹常見三種方式創(chuàng)建流程如下:

總結(jié):
- 最常用的三個方法:decodeFile, decodeResource, decodeStream,前兩個最終調(diào)用的是 decodeStream;
- **decodeStream, decodeByteArray, decodeFileDescription **這三個內(nèi)部則調(diào)用的是 native 方法來創(chuàng)建 Bitmap的【有種說法,Bitmap是Android中唯一通過 native 方法創(chuàng)建的類】;
- decodeResourceStream主要做了兩件事:一是對 opts.inDensity 賦值,沒有設置默認值 160;二是對 opts.inTargetDensity 賦值,沒有賦值為當前設備 densityDpi;
- decodeStream主要也做了兩件事:一是調(diào)用 native 方法解析 Bitmap;二是對解析得到的 Bitmap 調(diào)用 setDensityFraomOptions(bmp, opts) 進行設置;
- setDensityFraomOptions(bmp, opts)主要做了這樣幾件事:一是當opts.inDensity != opts.inTargetDensity || opts.inDensity != opts.inScreenDensity && (inScaled = true || isNinePatch) 時,將設置 outputBitmap.mDensity = inTargetDensity;
decodeResourceStream()方法源碼如下:
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
setDensityFromOptions(bmp, opts)源碼如下:
private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
if (outputBitmap == null || opts == null) return;
final int density = opts.inDensity;
if (density != 0) {
outputBitmap.setDensity(density);
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return;
}
byte[] np = outputBitmap.getNinePatchChunk();
final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
if (opts.inScaled || isNinePatch) {
outputBitmap.setDensity(targetDensity);
}
} else if (opts.inBitmap != null) {
// bitmap was reused, ensure density is reset
outputBitmap.setDensity(Bitmap.getDefaultDensity());
}
}
三、如何計算Bitmap占用內(nèi)存大小?
常規(guī)方式:
API方法:getByteCount() 獲取 - 不準確
粗略方式:
計算公式:圖片長 * 寬 * 4bytes/ARG_8888 - 不正確
通讀源碼得來的方式:
/**
* 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();
}
/**
* Return the number of bytes between rows in the bitmap's pixels. Note that
* this refers to the pixels as stored natively by the bitmap. If you call
* getPixels() or setPixels(), then the pixels are uniformly treated as
* 32bit values, packed according to the Color class.
*
* <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, this method
* should not be used to calculate the memory usage of the bitmap. Instead,
* see {@link #getAllocationByteCount()}.
*
* @return number of bytes between rows of the native bitmap pixels.
*/
public final int getRowBytes() {
if (mRecycled) {
Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
}
return nativeRowBytes(mNativePtr);
}
最終通過native源碼方法,可得到:一張ARGB_8888 的Bitmap占用內(nèi)存計算公式:bmpWidth * bmpHeight * 4byte。不是直接使用圖片分辨率進行計算,而是界面后 Bitmap 的寬高進行計算。
然而,這樣計算并不準確。有幾個不同的場景會導致最終計算的結(jié)果不正確。
- 將一張 720x1080 圖片分別放在不同分辨率drawable文件夾下,在同一個手機上加載;
- 也是同一張圖片放在指定分辨率的 drawable 文件夾下,在不同手機上加載;
- 切不同分辨率圖片到對應 drawable 文件夾下,在各分辨率設備上加載。
一般,我們讀取 drawable 目錄下的圖片,會用到 <code>decodeResource</code>獲取 Bitmap,該方法可以直接看上面提到的 decodeResourceStream() 方法源碼,通過源碼可知:
- 在讀取資源時,使用 openRawResource 方法,然后會對 TypedValue 進行賦值,其中包含了原始資源的 density 等信息,也即是文件夾代表的density;
- 調(diào)用 decodeResourceStream 對原始資源進行解碼和適配,實際是原始資源 density 到 設備屏幕 density 的映射。
這里看一下 資源文件夾代表的密度:

對照 decodeResourceStream() 源碼如何設置 opts.inDensity 邏輯:

最后通過查閱 native 源碼,得到計算公式:
一張圖片對應 Bitmap 占用內(nèi)存 = mBitmapHeight * mBitmapWidth * 4byte(ARGB_8888);
Native 方法中,mBitmapWidth = mOriginalWidth * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize,
mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize
現(xiàn)在針對介紹的幾種場景,會得到這樣的結(jié)論:
- 將一張 720x1080圖片放在 drawable-xhdpi 目錄下(inDensity = 320),
- 在 720x1080 手機上加載(inTargetDensity = 320),圖片不會被壓縮;
- 在 480x800 手機上加載(inTargetDensity = 240),圖片會被壓縮 9/16;
- 在 1080x1920 手機上加載(inTargetDensity = 480),圖片會被放大 2.25;
- 切不通分辨率大小的圖片放到對應文件夾下,會根據(jù)屏幕獲取對應文件夾的圖片,就不存在加載圖片時壓縮和放大(針對標準屏);
拓展問題:只切一套UI圖,是否適用?如何選擇?
注意,上述計算方式是在通過 decodeResource() 方法獲取 Bitmap 的情況下得出,其他幾種方式獲取Bitmap,最后得到占用內(nèi)存Size不會跟資源文件目錄相關(guān)聯(lián)。
四、問題解答
問題一:一張圖片對應 Bitmap 占用內(nèi)存 = mBitmapHeight * mBitmapWidth * 4byte(ARGB_8888);Native 方法中,mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize ;
由此可知,手機屏幕大小 1280 x 720(inTarget = 320),加載 xxhdpi (inDensity = 480)中的圖片 1920 x 1080,scale = 320 / 480,inSampleSize = 1,最終獲得的 Bitmap 的圖像大小是 :
mBitmapWidth = opts.outWidth = 1080 * (320 / 480) * 1/1 = 720,
mBitmapHeight = opt.outHeight = 1920 * (320 / 480) * 1/1 = 1280,
getAllocatedMemory() = mBitmapWidth * mBitmapHeight * 4 = Bitmap占用內(nèi)存。
問題三:使用 decodeResource() 和 decodeStream() 有什么區(qū)別?
(1)decodeResource() 流程,會先用 TypedValue 保存圖片信息,然后會根據(jù)條件設置 opts.inDensity = value.inDensity,為0則設置為默認 160dpi; 文件夾代表密度
Opts.inTargetDensity = getDisplayMetrics().densityDpi; 屏幕密度
設置完上述參數(shù)后,最終還是會調(diào)用 decodeStream() 方法;
(2)decodeStream() native 方法得到 Bitmap后,調(diào)用 setDensityFromOptions() 方法來設置 Bitmap.mDensity:
若 opts.inDensity != 0,bitmap.mDensity = opts.inDensity;
若 opts.inTargetDensity != 0 && inDensity != targetDensity && inDensity != screenDensity,繼續(xù)判斷,如果 opts.inScaled || isNinePatch,bitmap.mDensity = targetDensity;
所以,
(1)若使用 decodeResource() 加載本地圖片,inDensity 為加載圖片所在的文件夾代表的 dpi,inTargetDensity 為目標屏幕密度(or 圖片真實像素密度?),
最終 bitmap.mDensity = targetDensity。
(2)若使用 decodeStream() 則不會先記錄圖片信息,得到bitmap 后,直接調(diào)用 setDensityFromOptions() 方法,所以最終 bitmap.mDensity = defaultDensity() = DENSITY_DEVICE。
參考源碼API-26
參考:http://dev.qq.com/topic/591d61f56793d26660901b4e
???????????https://www.tuicool.com/articles/3eMNr2n
如有誤,請指正!