在開(kāi)發(fā)app時(shí),顯示一張本地圖片剪廉,這張圖片在加載時(shí)會(huì)占用大多內(nèi)存呢?猜測(cè)占用內(nèi)存大小和以下幾個(gè)因素有關(guān):
- 設(shè)計(jì)師切圖炕檩,圖片本身的分辨率斗蒋;
- 圖片所放文件夾代表的 密度 dpi;
- 手機(jī)自身的屏幕密度笛质;
- 經(jīng)過(guò)系統(tǒng)縮放得到的最終加載到手機(jī)上圖片的密度和占用的內(nèi)存泉沾。
我們知道Android中在加載本地大圖時(shí),很容易OOM妇押,主要原因在于加載的Bitmap占用內(nèi)存太大跷究。接下來(lái)將圍繞以下幾個(gè)問(wèn)題說(shuō)明如何計(jì)算一張Bitmap占用的內(nèi)存大小。
- 將一張分辨率為 720x1080 的圖片放到 xxhdpi 或者 hdpi 敲霍,同放在 xhdpi 標(biāo)準(zhǔn)文件夾下俊马,對(duì)于同一臺(tái)手機(jī)占用內(nèi)存大小是否有變化丁存?
- 同一張分辨率為 720x1080 的圖片被不同屏幕分辨率的手機(jī)加載,BitmapFactory 的成員變量 inDensity柴我、 inScreenDensity解寝、 inTargetDensity 會(huì)怎樣變化?這些值又是怎樣被賦值的艘儒,又是怎樣進(jìn)行縮放的聋伦?
- 使用 decodeResource() 和 decodeStream() 有什么區(qū)別?
- Options 的 inDensity界睁、 inTargetDensity 和 輸出的 Bitmap 的 mDensity 有什么關(guān)系觉增?Bitmap 的 mWidth、 mHeight 與 Options 的 outputWidth翻斟、 outputHeight 有什么關(guān)系逾礁?
- 這些同計(jì)算 Bitmap 內(nèi)存占用大小的 長(zhǎng)寬有什么關(guān)系?
在回答這些問(wèn)題之前杨赤,先介紹一下DisplayMetrics和Bitmap及其相關(guān)類(lèi)敞斋。
一、DisplayMetrics和Bitmap及其相關(guān)類(lèi)
DisplayMetrics
說(shuō)明:屏幕密度相關(guān)類(lèi)疾牲,可以用于獲取屏幕高和寬以及屏幕密度density植捎、每英寸點(diǎn)數(shù)densityDpi . 這里,density 數(shù)值為 1dp = density px阳柔;在 DisplayMetrics 中焰枢,這兩個(gè)是線性相關(guān):
Bitmap
說(shuō)明:Bitmap 在 Android 中指的是一張圖片,可以是 png舌剂,也可以是 jpg等其他圖片格式济锄。
作用:可以獲取圖像文件信息,對(duì)圖像進(jìn)行剪切霍转、旋轉(zhuǎn)荐绝、縮放、壓縮等操作避消,并可以指定格式保存圖像文件低滩。
Bitmap.Config
說(shuō)明:Bitmap 格式。除了尺寸外岩喷,影響一個(gè)圖片占用空間還有色彩細(xì)節(jié)恕沫。位圖位數(shù)越高表示可以存儲(chǔ)的顏色信息越多,圖像也就越清晰逼真纱意。
- 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
說(shuō)明:提供解析Bitmap的靜態(tài)工廠方法。
BitmapFactory.Options
說(shuō)明:用于解碼Bitmap時(shí)的各種參數(shù)控制虏两。
幾個(gè)重要參數(shù):
inBitmap:在解析Bitmap時(shí)重用該Bitmap愧旦,但是必須相同大小的Bitmap & inMutable = true 才可重用;
inMutable :配置Bitmap是否可更改定罢,如每隔幾個(gè)像素給Bmp添加一條直線笤虫;
inPreferredConfig:Config顏色位數(shù),默認(rèn)值為Bitmap.Config.ARGB_888祖凫;
inDither:是否抖動(dòng)琼蚯,默認(rèn)false(Android Depracated);
inPremultiplied:默認(rèn)true引矩,一般不改變其值荞驴。
inPurgeable:當(dāng)存儲(chǔ)像素內(nèi)存空間 在系統(tǒng)內(nèi)存不足時(shí) 是否可被回收(Android L Deprecated);
inInputShareable:是否可以共享一個(gè) InputStream (Android L Deprecated);
inPreferQualityOverSpeed:為true時(shí)會(huì)優(yōu)先保證 Bitmap 質(zhì)量,其次是解碼速度(Android N Deprecated);
inTempStorage:解碼時(shí)的臨時(shí)空間蓖乘,建議 16K稠屠;
inJustDecodeBounds:為true時(shí)僅返回 Bitmap 寬高等屬性峦睡,返回bmp=null,為false時(shí)才返回占內(nèi)存的 bmp权埠;
inSampleSize:表示 Bitmap 的壓縮比例榨了,值必須 > 1 & 是2的冪次方。inSampleSize = 2 時(shí)攘蔽,表示壓縮寬高各1/2龙屉,最后返回原始圖1/4大小的Bitmap;
inDensity:表示 Bitmap 像素密度满俗;
inTargetDensity:表示 Bitmap 最終的像素密度转捕;
inScreenDensity:表示當(dāng)前屏幕的像素密度;
inScaled:默認(rèn)為true漫雷,是否支持縮放瓜富,設(shè)置為true時(shí)鳍咱,Bitmap將以 inTargetDensity 的值進(jìn)行縮放降盹;
outputWidth:返回的 Bitmap的寬;
outputHeight:返回的 Bitmap的高。
以一張類(lèi)圖說(shuō)明Bitmap蓄坏、BitmapFactory和BitmapFactory.Options三者之間的關(guān)系价捧,如下圖所示:
二结蟋、ImageView 設(shè)置圖片 & Bitmap創(chuàng)建流程
ImageView 設(shè)置圖片
一般地,給 ImageView 設(shè)置資源圖片時(shí)渔彰,會(huì)用到四種方式:setImageResource(), setImageUri(), setImageBitmap(), setImageDrawable嵌屎。這四種方式有什么區(qū)別呢?用一張圖來(lái)展示:
總結(jié):由上可知恍涂,ImageView設(shè)置本地圖片會(huì)先生成 Bitmap 再將 Bitmap 轉(zhuǎn)成 Drawable宝惰,最終通過(guò) setImageDrawable() 設(shè)置;
【所以這步是否可以看做使用 setImageDrawable 會(huì)跳過(guò)讀取和解碼 Bitmap 操作再沧,為最優(yōu)設(shè)置本地圖片方式呢尼夺?
—— 需測(cè)試內(nèi)存占用情況方可驗(yàn)證〕慈常】
Bitmap創(chuàng)建流程
BitmapFactory 提供了五種方式來(lái)創(chuàng)建Bitmap淤堵,分別是:decodeFile, decodeResource, decodeByteArray, decodeStream, decodeFileDescription,這里只介紹常見(jiàn)三種方式創(chuàng)建流程如下:
總結(jié):
- 最常用的三個(gè)方法:decodeFile, decodeResource, decodeStream顷扩,前兩個(gè)最終調(diào)用的是 decodeStream;
- **decodeStream, decodeByteArray, decodeFileDescription **這三個(gè)內(nèi)部則調(diào)用的是 native 方法來(lái)創(chuàng)建 Bitmap的【有種說(shuō)法拐邪,Bitmap是Android中唯一通過(guò) native 方法創(chuàng)建的類(lèi)】;
- decodeResourceStream主要做了兩件事:一是對(duì) opts.inDensity 賦值屎即,沒(méi)有設(shè)置默認(rèn)值 160庙睡;二是對(duì) opts.inTargetDensity 賦值,沒(méi)有賦值為當(dāng)前設(shè)備 densityDpi技俐;
- decodeStream主要也做了兩件事:一是調(diào)用 native 方法解析 Bitmap乘陪;二是對(duì)解析得到的 Bitmap 調(diào)用 setDensityFraomOptions(bmp, opts) 進(jìn)行設(shè)置;
- setDensityFraomOptions(bmp, opts)主要做了這樣幾件事:一是當(dāng)opts.inDensity != opts.inTargetDensity || opts.inDensity != opts.inScreenDensity && (inScaled = true || isNinePatch) 時(shí)雕擂,將設(shè)置 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());
}
}
三、如何計(jì)算Bitmap占用內(nèi)存大芯摹谤逼?
常規(guī)方式:
API方法:getByteCount() 獲取 - 不準(zhǔn)確
粗略方式:
計(jì)算公式:圖片長(zhǎng) * 寬 * 4bytes/ARG_8888 - 不正確
通讀源碼得來(lái)的方式:
/**
* 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);
}
最終通過(guò)native源碼方法,可得到:一張ARGB_8888 的Bitmap占用內(nèi)存計(jì)算公式:bmpWidth * bmpHeight * 4byte仇穗。不是直接使用圖片分辨率進(jìn)行計(jì)算流部,而是界面后 Bitmap 的寬高進(jìn)行計(jì)算。
然而纹坐,這樣計(jì)算并不準(zhǔn)確枝冀。有幾個(gè)不同的場(chǎng)景會(huì)導(dǎo)致最終計(jì)算的結(jié)果不正確。
- 將一張 720x1080 圖片分別放在不同分辨率drawable文件夾下,在同一個(gè)手機(jī)上加載果漾;
- 也是同一張圖片放在指定分辨率的 drawable 文件夾下球切,在不同手機(jī)上加載;
- 切不同分辨率圖片到對(duì)應(yīng) drawable 文件夾下绒障,在各分辨率設(shè)備上加載吨凑。
一般,我們讀取 drawable 目錄下的圖片户辱,會(huì)用到 <code>decodeResource</code>獲取 Bitmap鸵钝,該方法可以直接看上面提到的 decodeResourceStream() 方法源碼,通過(guò)源碼可知:
- 在讀取資源時(shí)庐镐,使用 openRawResource 方法蒋伦,然后會(huì)對(duì) TypedValue 進(jìn)行賦值,其中包含了原始資源的 density 等信息焚鹊,也即是文件夾代表的density痕届;
- 調(diào)用 decodeResourceStream 對(duì)原始資源進(jìn)行解碼和適配,實(shí)際是原始資源 density 到 設(shè)備屏幕 density 的映射末患。
這里看一下 資源文件夾代表的密度:
對(duì)照 decodeResourceStream() 源碼如何設(shè)置 opts.inDensity 邏輯:
最后通過(guò)查閱 native 源碼研叫,得到計(jì)算公式:
一張圖片對(duì)應(yīng) 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)在針對(duì)介紹的幾種場(chǎng)景,會(huì)得到這樣的結(jié)論:
- 將一張 720x1080圖片放在 drawable-xhdpi 目錄下(inDensity = 320)探橱,
- 在 720x1080 手機(jī)上加載(inTargetDensity = 320)申屹,圖片不會(huì)被壓縮;
- 在 480x800 手機(jī)上加載(inTargetDensity = 240)隧膏,圖片會(huì)被壓縮 9/16哗讥;
- 在 1080x1920 手機(jī)上加載(inTargetDensity = 480),圖片會(huì)被放大 2.25胞枕;
- 切不通分辨率大小的圖片放到對(duì)應(yīng)文件夾下杆煞,會(huì)根據(jù)屏幕獲取對(duì)應(yīng)文件夾的圖片,就不存在加載圖片時(shí)壓縮和放大(針對(duì)標(biāo)準(zhǔn)屏);
拓展問(wèn)題:只切一套UI圖腐泻,是否適用决乎?如何選擇?
注意派桩,上述計(jì)算方式是在通過(guò) decodeResource() 方法獲取 Bitmap 的情況下得出构诚,其他幾種方式獲取Bitmap,最后得到占用內(nèi)存Size不會(huì)跟資源文件目錄相關(guān)聯(lián)铆惑。
四范嘱、問(wèn)題解答
問(wèn)題一:一張圖片對(duì)應(yīng) Bitmap 占用內(nèi)存 = mBitmapHeight * mBitmapWidth * 4byte(ARGB_8888)凳寺;Native 方法中,mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize 彤侍;
由此可知,手機(jī)屏幕大小 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)存魄眉。
問(wèn)題三:使用 decodeResource() 和 decodeStream() 有什么區(qū)別砰盐?
(1)decodeResource() 流程,會(huì)先用 TypedValue 保存圖片信息坑律,然后會(huì)根據(jù)條件設(shè)置 opts.inDensity = value.inDensity岩梳,為0則設(shè)置為默認(rèn) 160dpi; 文件夾代表密度
Opts.inTargetDensity = getDisplayMetrics().densityDpi晃择; 屏幕密度
設(shè)置完上述參數(shù)后冀值,最終還是會(huì)調(diào)用 decodeStream() 方法;
(2)decodeStream() native 方法得到 Bitmap后宫屠,調(diào)用 setDensityFromOptions() 方法來(lái)設(shè)置 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 為目標(biāo)屏幕密度(or 圖片真實(shí)像素密度?)缰猴,
最終 bitmap.mDensity = targetDensity绢慢。
(2)若使用 decodeStream() 則不會(huì)先記錄圖片信息,得到bitmap 后洛波,直接調(diào)用 setDensityFromOptions() 方法胰舆,所以最終 bitmap.mDensity = defaultDensity() = DENSITY_DEVICE。
參考源碼API-26
參考:http://dev.qq.com/topic/591d61f56793d26660901b4e
???????????https://www.tuicool.com/articles/3eMNr2n
如有誤蹬挤,請(qǐng)指正缚窿!