在平常的開(kāi)發(fā)中,經(jīng)常容易遇到的問(wèn)題便是OOM的內(nèi)存泄漏,而在泄漏的過(guò)程中,圖片的問(wèn)題一般占據(jù)榜首位置,即便在當(dāng)前已經(jīng)有了諸多優(yōu)秀開(kāi)源的圖片緩存框架的情況下,有時(shí)候依舊不可避免.圖片的加載消耗內(nèi)存,大量的圖片進(jìn)行內(nèi)存消耗,使用以后不加以回收等等都是導(dǎo)致圖片內(nèi)存泄漏的問(wèn)題所在.
這時(shí)需要我們來(lái)理解圖片的內(nèi)存使用情況,如何來(lái)解決問(wèn)題.
圖片由一個(gè)個(gè)的像素點(diǎn)構(gòu)成,加載過(guò)程會(huì)創(chuàng)建一個(gè)二維數(shù)組,在數(shù)組中圖片分辨率為x,y,每一個(gè)像素點(diǎn)由ARGB組成,占據(jù)4個(gè)字節(jié)因此常理來(lái)說(shuō)消耗的內(nèi)存應(yīng)該為:
1KB=1024Byte 1MB= 1024Byte*1024= 1048576Byte
消耗內(nèi)存大小=分辨率x * 分辨率y * 4byte=??Byte
我們來(lái)來(lái)觀察一張1080*1920的圖片的在各個(gè)文件夾下的內(nèi)存消耗狀況.
-
首先看看密度,密度值,代表分辨率之間的關(guān)系
密度 密度值 分辨率 mdpi 120dpi ~ 160dpi 320 * 480 hdpi 160dpi ~ 240dpi 480 * 720 xhdpi 240dpi ~ 320dpi 720 * 1280 xxhdpi 320dpi ~ 480dpi 1080 * 1920 xxxdhpi 480dpi ~ 640dpi 1440 * 2560
xxhdpi下的顯示
直接加載資源圖片
<ImageView
android:background="@color/colorAccent"
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
imageView.setImageResource(R.drawable.gyy1080);
內(nèi)存占用大小:15.12MB

整個(gè)imageview控件占據(jù)大小位置:

可以看出所占內(nèi)存為15.12MB.按照之前的公式1920 * 1080 * 4byte約等于8MB,可是我們這里怎么消耗了差點(diǎn)2倍?
這是我們先考慮是否因?yàn)樽陨硎謾C(jī)的dpi不屬于xxhdpi范圍.
檢測(cè)手機(jī)的屏幕密度值
xdpi = getResources().getDisplayMetrics().xdpi;
ydpi = getResources().getDisplayMetrics().ydpi;
Log.e("密度值","xdpi: " +xdpi + "--"+"ydpi: "+ydpi + "");
打印結(jié)果
E/密度值: xdpi: 640.0--ydpi: 640.0
從打印結(jié)果中我們得知圖片的密度值屬于xxxdhpi。
下面我們將xxhdpi中的圖片放置到xxxhdpi中觀察結(jié)果
xxxhdpi下的顯示
xxxhdpi下圖片大小:

xxxhdpi下內(nèi)存占用大小:

從現(xiàn)實(shí)結(jié)果上可以看出8.99MB和我們預(yù)計(jì)的8MB的出入大小已經(jīng)很接近了.多出的0.99MB主要是由于圖片的EXIF也還有一定的信息數(shù)據(jù),所以實(shí)際會(huì)比我們預(yù)計(jì)的大小要大.
并且圖片所占據(jù)屏幕的大小也有所改變,這是我們猜測(cè)是否是圖片被系統(tǒng)自動(dòng)改變了圖片控件大小,我們繼續(xù)測(cè)試,跳過(guò)xhpdi,將圖片放到hdpi下測(cè)試
hdpi下的顯示
hdpi下圖片大小:

hdpi下內(nèi)存占用大小:

這時(shí)候我們發(fā)現(xiàn)更恐怖的事情發(fā)生了,圖片控件充斥滿了整個(gè)屏幕不說(shuō),內(nèi)存更恐怖的消耗達(dá)到了57.14MB,要知道這僅僅只是一張圖片,要是有更多的圖片這樣豈不是爆炸...
部分總結(jié)
經(jīng)過(guò)上述3個(gè)簡(jiǎn)單的圖片測(cè)試,我們可以得出一個(gè)簡(jiǎn)單的結(jié)論:
- UI在設(shè)計(jì)時(shí)也應(yīng)該盡量以當(dāng)前市場(chǎng)的主流密度來(lái)作為設(shè)計(jì)(比如當(dāng)前是1080P,后續(xù)可能就是2K了),并且程序猿在圖片的放置位置應(yīng)該盡量放置在高密度的文件夾中,以此來(lái)減少內(nèi)存的開(kāi)銷
有的人說(shuō)為什么要設(shè)計(jì)主流的密度?
如果是超過(guò)了主流密度的圖片本身已經(jīng)很大了,對(duì)于內(nèi)存消耗是一樣的,并且過(guò)大在低分辨率的機(jī)型顯示會(huì)最大化的占據(jù)識(shí)圖空間
如果是設(shè)計(jì)的尺寸低于主流市場(chǎng)的密度過(guò)多,又會(huì)導(dǎo)致圖片在高分辨率機(jī)型上縮小,并且在放大后會(huì)看見(jiàn)明顯的模糊狀況.
因此盡量設(shè)計(jì)主流密度來(lái)完成開(kāi)發(fā).
OK...你以為到這里就結(jié)束了...NO NO NO. 有的時(shí)候即便我們的圖片放在最頂級(jí)的文件夾中,但是因?yàn)閳D片本身巨大,根本無(wú)法讀取加載,也是必然的OOM
大圖加載
這里我使用一張3500 * 5250的圖片來(lái)進(jìn)行加載,按照常規(guī)方式加載
imageView.setImageResource(R.drawable.biggyy3500);

直接就OOM爆炸了.我們不禁想怎么辦?
如何加載大分辨率圖片
對(duì)于大分辨率圖片而言,手機(jī)即便成將其加載出來(lái),那么消耗的內(nèi)存也是巨大的,在移動(dòng)設(shè)備上來(lái)說(shuō)內(nèi)存是很可貴的,你用了這么多,別的地方要使用內(nèi)存怎么辦呢,所以我們可以將圖片進(jìn)行壓縮,來(lái)降低他的分辨率,適配當(dāng)前的手機(jī)然后在進(jìn)行加載.
既保證了內(nèi)存的開(kāi)銷又保證了圖片的分辨率適應(yīng)當(dāng)前設(shè)備.
要改變圖片的分辨率,我們需要用到BitmapFactory.Options,使用它獲取圖片的信息并且根據(jù)當(dāng)前的設(shè)備進(jìn)行壓縮采樣生成新的Drawable來(lái)進(jìn)行使用.
BitmapFactory.Options options = new BitmapFactory.Options();
// 不讀取像素?cái)?shù)組到內(nèi)存中,僅讀取圖片的信息
options.inJustDecodeBounds = true;
// 獲取圖片大小
BitmapFactory.decodeResource(resource, resId, options);
// 從Options中獲取圖片的分辨率
int srcWidth = options.outWidth;
int srcHeight = options.outHeight;
boolean densityFaking = false;
if (options.inDensity < resource.getDisplayMetrics().densityDpi) {
// 相同的density不會(huì)scale放大
options.inDensity = resource.getDisplayMetrics().densityDpi;
densityFaking = true;
if (DEBUG_SCALE) {
Log.d(TAG, "set inDensity=" + resource.getDisplayMetrics().densityDpi);
}
} else {
// 根據(jù)density計(jì)算scale縮小之后寬高
srcWidth = scaleFromDensity(srcWidth, options.inDensity, options.inTargetDensity);
srcHeight = scaleFromDensity(srcHeight, options.inDensity, options.inTargetDensity);
if (DEBUG_SCALE) {
Log.d(TAG, "scaleFromDensity srcWidth=" + srcWidth + " srcHeight=" + srcHeight);
}
}
ImageSize srcSize = new ImageSize(srcWidth, srcHeight);
ImageSize tarSize = new ImageSize(Constants.DISPLAY_WIDTH, Constants.DISPLAY_HEIGHT);
// 根據(jù)density計(jì)算scale之后的寬高才是準(zhǔn)確的采樣源大小
// 計(jì)算采樣率,縮小圖片
int inSampleSize = ImageSizeUtils.computeImageSampleSize(srcSize, tarSize, ViewScaleType.FIT_INSIDE, true);
if (useRgb565) {
if (DEBUG_SCALE) {
Log.d(TAG, "PreferredConfig use RGB565");
}
// 通常機(jī)型能根據(jù)圖片是否有Alpha通道來(lái)決定是否真正使用RGB_565,但有的機(jī)型是強(qiáng)制應(yīng)用,所以RGB_565還是得慎重使用
options.inPreferredConfig = Bitmap.Config.RGB_565;
} else if (!densityFaking && inSampleSize == 1) {
// 不需要壓縮,也不需要采樣,直接返回null,由外部處理
if (DEBUG_SCALE) {
Log.d(TAG, "No scaling and no sampling, just return");
}
return null;
}
options.inSampleSize = inSampleSize;
// 讀取圖片像素?cái)?shù)組到內(nèi)存中,設(shè)定的采樣率
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(resource, resId, options);
return bitmap;
代碼的核心在于使用BitmapFactory.Options獲取到了圖片一系列的信息,根據(jù)圖片的信息和設(shè)備的分辨率作比較,判斷是否進(jìn)行縮放,以及如何縮放.
在縮放的處理上可以自行實(shí)現(xiàn)或者借鑒ImageLoader的核心計(jì)算縮放的方法.
自行簡(jiǎn)單計(jì)算采樣率:
// 計(jì)算采樣率
int scaleX = 圖片寬分辨率 / 設(shè)備寬分辨率;
int scaleY = 圖片高分辨率 / 設(shè)備高分辨率;
int inSampleSize = 1;
if (scaleX > scaleY && scaleY >= 1) {
inSampleSize = scaleX;
}
if (scaleX < scaleY && scaleX >= 1) {
inSampleSize = scaleY;
}
在這里我使用ImageLoder的計(jì)算采樣方法(有現(xiàn)成的干嗎不用)
- computeImageSampleSize
通過(guò)對(duì)源圖片的寬高和目標(biāo)圖片的寬高(設(shè)備的分辨率)進(jìn)行循環(huán)壓縮判斷,直到獲取到一個(gè)適合當(dāng)前屏幕比例的采樣率。并且在ImagView中因?yàn)橛袌D片的樣式風(fēng)格還加入了ScaleType的區(qū)別處理,簡(jiǎn)直業(yè)界良心
獲得采樣率之后就可以將圖片重新設(shè)置采樣率輸出Bitmap。
獲取壓縮后的Drawanle
BitmapDrawable drawable = new BitmapDrawable(bitmap);
drawable.setTargetDensity(resource.getDisplayMetrics().densityDpi);
壓縮后的Drawable和設(shè)備的分辨率保持一致性.
這里我們只是獲取了Drawable,如果是一些常用的甚至可以使用弱引用將其緩存下來(lái),注意緩存的時(shí)候需要緩存的是Bitmap,而不是Drawable
- 緩存Bitmap
private static HashMap<String, WeakReference<Bitmap>> stringWeakReferenceBitmap =
new HashMap<String, WeakReference<Bitmap>>();
// 緩存Bitmap對(duì)象
stringWeakReferenceBitmap.put(key, new WeakReference<Bitmap>(bitmap));
- 獲取緩存的BitMap
// 從弱引用緩存中獲取
WeakReference<Bitmap> ref = stringWeakReferenceBitmap.get(key);
使用重新采樣后的drawable
直接加載圖片而不緩存
imageView.setImageDrawable( ResourceUtils.getScaledDrawable(getResources(),R.drawable.biggyy3500));

可以看出我即便這張圖片的分辨率達(dá)到3500 * 5250,在經(jīng)過(guò)壓縮重新采樣適配當(dāng)前設(shè)備后,依然將其加載出來(lái)了,并且內(nèi)存消耗僅有5MB.
總結(jié)
- 設(shè)計(jì)師設(shè)計(jì)圖片要根據(jù)主流分辨率設(shè)計(jì)
- 攻城獅在放置圖片時(shí)要根據(jù)設(shè)計(jì)師設(shè)計(jì)的圖片分辨率來(lái)選擇正確的文件夾并且盡量選擇高分辨率的文件夾
- 如果有低分辨率的圖片而運(yùn)行在高分辨率機(jī)型OOM崩潰需要進(jìn)行圖片的重新壓縮采樣處理即可解決內(nèi)存問(wèn)題