Bitmap版本演變
Bitmap的處理是Android開發(fā)過程中無法避開的一項(xiàng),也是內(nèi)存占用的大戶,常見的內(nèi)存占用優(yōu)化都涉及到Bitmap。
Android SDK各版本也一直在對(duì)Bitmap進(jìn)行優(yōu)化:
Android3.0以前Bitmap像素存在Native區(qū),生命周期不可控,需手動(dòng)回收bitmap.recycle()
Android3.0-8.0 Bitmap像素存在Java堆,虛擬機(jī)自動(dòng)回收,但容易OOM
Android8.0及以后Bitmap像素存在Native區(qū)域,配合NativeAllocationRegistry機(jī)制+系統(tǒng)支持,虛擬機(jī)自動(dòng)回收
為什么會(huì)有這種變化?首先Bitmap對(duì)象占用內(nèi)存很小,主要是像素信息占用內(nèi)存很大。
Bitmap在3.0以前Native區(qū)的回收是依賴Java虛擬機(jī)的GC來回收的。Bitmap在Java堆上的內(nèi)存占用很小,假如應(yīng)用增加了很多圖片,在這個(gè)過程中Java堆上的內(nèi)存上漲是不明顯的;在沒有內(nèi)存增長(zhǎng)觸發(fā)GC時(shí),虛擬機(jī)自身的回收周期很漫長(zhǎng),這時(shí)就會(huì)出現(xiàn)系統(tǒng)內(nèi)存已經(jīng)不夠用了,而Java堆依然無法觸發(fā)GC,直到系統(tǒng)內(nèi)存OOM。以前的手機(jī)大家可能遇到過應(yīng)用無提示閃退、桌面崩潰、手機(jī)自動(dòng)關(guān)機(jī)的情況,這大都是系統(tǒng)內(nèi)存OOM造成的,系統(tǒng)內(nèi)存OOM時(shí),會(huì)殺后臺(tái)進(jìn)程,殺服務(wù)(部分廠商定制有問題導(dǎo)致手機(jī)關(guān)機(jī)),殺前臺(tái)進(jìn)程直到內(nèi)存釋放出來。所以3.0以前在不使用Bitmap時(shí),需要我們手動(dòng)調(diào)用recycle()釋放Native內(nèi)存。
但手動(dòng)釋放內(nèi)存不符合JAVA一直宣揚(yáng)的把程序員的雙手從內(nèi)存管理中解脫出來的氣質(zhì)啊。所以3.0版本把像素信息放到Java堆上了,這樣就可以觸發(fā)Java虛擬機(jī)的GC機(jī)制,不需要程序員手動(dòng)調(diào)用recycle()釋放,但是也更容易造成應(yīng)用OOM了。我們知道虛擬機(jī)的內(nèi)存使用上限都不大,3.0-8.0版本期間的手機(jī)虛擬機(jī)上限一般都是192M、256M。假如應(yīng)用加載幾張過大的圖片或者圖片引用有問題,直接就會(huì)導(dǎo)致OOM。
面對(duì)手機(jī)越來越大的內(nèi)存配置,如果還用虛擬機(jī)內(nèi)存限制應(yīng)用開發(fā),勢(shì)必是爭(zhēng)奪不過iOS的。所以8.0版本開始Google像iOS一樣,把內(nèi)存占用較大的像素信息重新放到Native區(qū),配合NativeAllocationRegistry機(jī)制+系統(tǒng)支持做到自動(dòng)回收。NativeAllocationRegistry機(jī)制在7.0版本已經(jīng)引入了,但是系統(tǒng)不支持,所以8.0版本才把像素信息放到Native區(qū)。
還有一點(diǎn)是recycle()調(diào)用還有沒有必要?如果圖片確認(rèn)不使用的話,還是有必要調(diào)用recycle()的,不管哪個(gè)系統(tǒng)版本,recycle()調(diào)用都會(huì)直接回收Native內(nèi)存。
Android3.0以前版本不用說必須調(diào)用
Android3.0-8.0版本雖然像素信息放到Java堆中,但是Bitmap創(chuàng)建是在native層解碼的,還是有少量?jī)?nèi)存占用在Native層
Android8.0及以后版本系統(tǒng)對(duì)Bitmap的內(nèi)存占用做了優(yōu)化監(jiān)聽,但還是依賴GC來回收的,而GC是非實(shí)時(shí)回收的
另外一個(gè)變化比較大的是Bitmap的內(nèi)存復(fù)用
Android4.4之前版本內(nèi)存復(fù)用要求width=width&&height=height&&inSampleSize = 1
Android4.4及之后版本只要被復(fù)用bitmap內(nèi)存大于所需內(nèi)存即可
這里內(nèi)存復(fù)用有個(gè)需要注意的地方,內(nèi)存復(fù)用的是bitmap整塊內(nèi)存,而不是所需要的內(nèi)存。
比如當(dāng)前圖片大小是1M,復(fù)用的Bitmap內(nèi)存大小是4M,復(fù)用這塊內(nèi)存之后,當(dāng)前圖片所占內(nèi)存為4M,只要當(dāng)前圖片沒被釋放,所占用的4M內(nèi)存是無法被釋放的。所以復(fù)用內(nèi)存時(shí)要找大小相當(dāng)?shù)膬?nèi)存使用。
Glide對(duì)Bitmap的處理
Glide非常強(qiáng)大,基本上我們能做的圖片優(yōu)化,Glide默認(rèn)都幫我們做了。
關(guān)于Glide對(duì)圖片緩存的處理網(wǎng)上有很多文章。這里只說一下內(nèi)存優(yōu)化相關(guān)的三塊:Bitmap自動(dòng)適配Imageview、內(nèi)存復(fù)用和Bitmap解碼的處理。以Glide:4.9.0版本作為源碼分析,因?yàn)镚lide的框架很龐大,調(diào)用鏈很長(zhǎng)(看過Glide源碼的應(yīng)該知道),就不貼調(diào)用鏈代碼了,只把關(guān)鍵代碼貼出來。
Glide中Bitmap的自動(dòng)適配Imageview。
// 計(jì)算縮小比例是否大于2或者2的倍數(shù),如果大于2或者2的倍數(shù)先設(shè)置inSampleSize屬性
int outWidth = round(exactScaleFactor * sourceWidth);
int outHeight = round(exactScaleFactor * sourceHeight);
int widthScaleFactor = sourceWidth / outWidth;
int heightScaleFactor = sourceHeight / outHeight;
int scaleFactor = rounding == SampleSizeRounding.MEMORY
? Math.max(widthScaleFactor, heightScaleFactor)
: Math.min(widthScaleFactor, heightScaleFactor);
int powerOfTwoSampleSize;
// BitmapFactory does not support downsampling wbmp files on platforms <= M. See b/27305903.
if (Build.VERSION.SDK_INT <= 23
&& NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) {
powerOfTwoSampleSize = 1;
} else {
powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor));
if (rounding == SampleSizeRounding.MEMORY
&& powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
powerOfTwoSampleSize = powerOfTwoSampleSize << 1;
}
}
options.inSampleSize = powerOfTwoSampleSize;
// 如果縮小2的倍數(shù)后仍需要縮小,利用inDensity和inTargetDensity繼續(xù)縮小
int powerOfTwoWidth;
int powerOfTwoHeight;
if (imageType == ImageType.JPEG) {
// libjpegturbo can downsample up to a sample size of 8. libjpegturbo uses ceiling to round.
// After libjpegturbo's native rounding, skia does a secondary scale using floor
// (integer division). Here we replicate that logic.
int nativeScaling = Math.min(powerOfTwoSampleSize, 8);
powerOfTwoWidth = (int) Math.ceil(sourceWidth / (float) nativeScaling);
powerOfTwoHeight = (int) Math.ceil(sourceHeight / (float) nativeScaling);
int secondaryScaling = powerOfTwoSampleSize / 8;
if (secondaryScaling > 0) {
powerOfTwoWidth = powerOfTwoWidth / secondaryScaling;
powerOfTwoHeight = powerOfTwoHeight / secondaryScaling;
}
} else if (imageType == ImageType.PNG || imageType == ImageType.PNG_A) {
powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize);
} else if (imageType == ImageType.WEBP || imageType == ImageType.WEBP_A) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
powerOfTwoWidth = Math.round(sourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = Math.round(sourceHeight / (float) powerOfTwoSampleSize);
} else {
powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize);
powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize);
}
} else if (
sourceWidth % powerOfTwoSampleSize != 0 || sourceHeight % powerOfTwoSampleSize != 0) {
// If we're not confident the image is in one of our types, fall back to checking the
// dimensions again. inJustDecodeBounds decodes do obey inSampleSize.
int[] dimensions = getDimensions(is, options, decodeCallbacks, bitmapPool);
// Power of two downsampling in BitmapFactory uses a variety of random factors to determine
// rounding that we can't reliably replicate for all image formats. Use ceiling here to make
// sure that we at least provide a Bitmap that's large enough to fit the content we're going
// to load.
powerOfTwoWidth = dimensions[0];
powerOfTwoHeight = dimensions[1];
} else {
powerOfTwoWidth = sourceWidth / powerOfTwoSampleSize;
powerOfTwoHeight = sourceHeight / powerOfTwoSampleSize;
}
double adjustedScaleFactor = downsampleStrategy.getScaleFactor(
powerOfTwoWidth, powerOfTwoHeight, targetWidth, targetHeight);
// Density scaling is only supported if inBitmap is null prior to KitKat. Avoid setting
// densities here so we calculate the final Bitmap size correctly.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor);
options.inDensity = getDensityMultiplier(adjustedScaleFactor);
}
if (isScaling(options)) {
options.inScaled = true;
} else {
options.inDensity = options.inTargetDensity = 0;
}
計(jì)算bitmap的縮放在com.bumptech.glide.load.resource.bitmap.Downsampler#calculateScaling方法中。Bitmap縮小計(jì)算主要分兩步,首先計(jì)算縮小比例是否大于2或者2的倍數(shù),如果大于2或者2的倍數(shù),先利用inSampleSize屬性進(jìn)行縮小,如果縮小之后的尺寸仍然大于imageview尺寸,再利用inDensity和inTargetDensity繼續(xù)縮小。通過這種方式可以嚴(yán)格縮小到我們所需要的尺寸。比如一張圖片大小是1000 * 1000,我們展示的imageView大小是200 * 200,那么首先計(jì)算出inSampleSize = 4縮小到250 * 250,仍然大于我們需要的200 * 200,再利用inDensity和inTargetDensity計(jì)算出一個(gè)0.8的系數(shù)縮小到200 * 200。
Glide中Bitmap內(nèi)存復(fù)用
Glide默認(rèn)支持了Bitmap的內(nèi)存復(fù)用。Glide內(nèi)存復(fù)用的處理是這樣的,在解碼一張圖片的尺寸后,先去bitmapPool中查找有沒有可以復(fù)用的內(nèi)存,如果有直接拿來復(fù)用,如果沒有,會(huì)先申請(qǐng)一塊沒有像素信息的內(nèi)存,然后把這塊沒有像素信息的bitmap內(nèi)存復(fù)用給我們需要解碼的圖片。
public Bitmap getDirty(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result == null) {
result = createBitmap(width, height, config);
}
return result;
}
可以看到查找和創(chuàng)建時(shí)都傳入了三個(gè)參數(shù)width、height和config,通過width、height和config中的outConfig計(jì)算來所需內(nèi)存大小。后面有個(gè)Glide中Bitmap解碼的坑會(huì)用到這塊。
Glide中Bitmap解碼
我們通過inPreferredConfig來配置Bitmap的解碼方式,系統(tǒng)默認(rèn)使用的是ARGB-8888解碼方式,這個(gè)配置選項(xiàng)只是建議解碼方式,系統(tǒng)真正解碼不一定會(huì)按照我們配置的參數(shù)來解碼,比如說如果圖片有alpha通道,就會(huì)強(qiáng)制使用ARGB-8888解碼;還有8.0版本之后的硬拉位圖更為特殊,會(huì)使很多配置無法生效。
Glide4.0之前版本默認(rèn)使用RGB-565解碼,之后默認(rèn)使用ARGB-8888解碼。但是Glide提供了配置解碼方式的api,如果我們想利用系統(tǒng)解碼的特點(diǎn)通過配置RGB-565的方式來減少bitmap的內(nèi)存占用,會(huì)發(fā)現(xiàn)一直無法“生效”。
下面來看一下Glide配置Bitmap解碼方式的邏輯。
private void calculateConfig(
InputStream is,
DecodeFormat format,
boolean isHardwareConfigAllowed,
boolean isExifOrientationRequired,
BitmapFactory.Options optionsWithScaling,
int targetWidth,
int targetHeight) {
if (hardwareConfigState.setHardwareConfigIfAllowed(
targetWidth,
targetHeight,
optionsWithScaling,
format,
isHardwareConfigAllowed,
isExifOrientationRequired)) {
return;
}
// Changing configs can cause skewing on 4.1, see issue #128.
if (format == DecodeFormat.PREFER_ARGB_8888
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
optionsWithScaling.inPreferredConfig = Bitmap.Config.ARGB_8888;
return;
}
boolean hasAlpha = false;
try {
hasAlpha = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool).hasAlpha();
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Cannot determine whether the image has alpha or not from header"
+ ", format " + format, e);
}
}
optionsWithScaling.inPreferredConfig =
hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
if (optionsWithScaling.inPreferredConfig == Config.RGB_565) {
optionsWithScaling.inDither = true;
}
}
可以看到,首先判斷是否有硬拉位圖的配置,如果有直接返回。然后是否為ARGB-8888或者有alpha通道,如果沒有alpha通道配置為RGB-565解碼。我們配置解碼方式為RGB-565并且使用一張沒有alpha通道的圖片,最終解碼方式為RGB-565,看一下為什么不“生效”。
下面是Glide解碼的大部分配置處理,包括尺寸縮小,第12行就是上面的配置解碼方式。
private Bitmap decodeFromWrappedStreams(InputStream is,
BitmapFactory.Options options, DownsampleStrategy downsampleStrategy,
DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth,
int requestedHeight, boolean fixBitmapToRequestedDimensions,
DecodeCallbacks callbacks) throws IOException {
long startTime = LogTime.getLogTime();
int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool);
....
calculateConfig(
is,
decodeFormat,
isHardwareConfigAllowed,
isExifOrientationRequired,
options,
targetWidth,
targetHeight);
boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
int expectedWidth;
int expectedHeight;
if (sourceWidth >= 0 && sourceHeight >= 0
&& fixBitmapToRequestedDimensions && isKitKatOrGreater) {
expectedWidth = targetWidth;
expectedHeight = targetHeight;
} else {
float densityMultiplier = isScaling(options)
? (float) options.inTargetDensity / options.inDensity : 1f;
int sampleSize = options.inSampleSize;
int downsampledWidth = (int) Math.ceil(sourceWidth / (float) sampleSize);
int downsampledHeight = (int) Math.ceil(sourceHeight / (float) sampleSize);
expectedWidth = Math.round(downsampledWidth * densityMultiplier);
expectedHeight = Math.round(downsampledHeight * densityMultiplier);
....
if (expectedWidth > 0 && expectedHeight > 0) {
setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);
}
}
Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
callbacks.onDecodeComplete(bitmapPool, downsampled);
....
return rotated;
}
如果服務(wù)端返回的圖片大小和前端所需展示的相差不大,那么這里inSampleSize就是1。然后會(huì)進(jìn)入第41行,這個(gè)方法就是內(nèi)存復(fù)用了??匆幌聝?nèi)存復(fù)用的邏輯。
private static void setInBitmap(
BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
@Nullable Bitmap.Config expectedConfig = null;
// Avoid short circuiting, it appears to break on some devices.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (options.inPreferredConfig == Config.HARDWARE) {
return;
}
// On API 26 outConfig may be null for some images even if the image is valid, can be decoded
// and outWidth/outHeight/outColorSpace are populated (see b/71513049).
expectedConfig = options.outConfig;
}
if (expectedConfig == null) {
// We're going to guess that BitmapFactory will return us the config we're requesting. This
// isn't always the case, even though our guesses tend to be conservative and prefer configs
// of larger sizes so that the Bitmap will fit our image anyway. If we're wrong here and the
// config we choose is too small, our initial decode will fail, but we will retry with no
// inBitmap which will succeed so if we're wrong here, we're less efficient but still correct.
expectedConfig = options.inPreferredConfig;
}
// BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
}
如果是8.0及以上系統(tǒng)并且是硬拉位圖直接返回,否則使用options.outConfig。這里就要出問題了,options.outConfig這個(gè)值是非空的,而且是ARGB-8888,這個(gè)參數(shù)是什么時(shí)候賦值的呢?
private static int[] getDimensions(InputStream is, BitmapFactory.Options options,
DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool) throws IOException {
options.inJustDecodeBounds = true;
decodeStream(is, options, decodeCallbacks, bitmapPool);
options.inJustDecodeBounds = false;
return new int[] { options.outWidth, options.outHeight };
}
這個(gè)方法應(yīng)該還記得吧,在上面解碼方法的第一行就是這個(gè)方法用來獲取圖片的尺寸。而獲取尺寸時(shí)傳入的options是沒有inPreferredConfig值的,后面會(huì)說到為什么。還記得上面說的如果沒有配置解碼方式,系統(tǒng)默認(rèn)使用ARGB-8888解碼嗎,所以這里獲取到尺寸后options的outConfig就被賦值了ARGB-8888。
回到上面來,8.0及以后版本,這個(gè)值肯定不為空,導(dǎo)致無法被賦值我們配置的inPreferredConfig。然后調(diào)用bitmapPool.getDirty(width, height, expectedConfig)去獲取要復(fù)用內(nèi)存的bitmap。上面說內(nèi)存復(fù)用時(shí)提到過內(nèi)存復(fù)用通過width、height和outConfig來計(jì)算需要的內(nèi)存大小,這里計(jì)算的大小就是ARGB-8888解碼4個(gè)字節(jié)的大小啦。還記得系統(tǒng)內(nèi)存復(fù)用的機(jī)制嗎,內(nèi)存復(fù)用是復(fù)用整個(gè)bitmap的內(nèi)存,所以這里的內(nèi)存大小就是ARGB-8888解碼格式的大小了。
下面來看一下我們配置的inPreferredConfig為什么在我們解碼尺寸的時(shí)候沒有被使用到呢?
public Resource<Bitmap> decode(InputStream is, int requestedWidth, int requestedHeight,
Options options, DecodeCallbacks callbacks) throws IOException {
Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports"
+ " mark()");
byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
bitmapFactoryOptions.inTempStorage = bytesForOptions;
DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
boolean isHardwareConfigAllowed =
options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG);
try {
Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth,
requestedHeight, fixBitmapToRequestedDimensions, callbacks);
return BitmapResource.obtain(result, bitmapPool);
} finally {
releaseOptions(bitmapFactoryOptions);
byteArrayPool.put(bytesForOptions);
}
}
private static synchronized BitmapFactory.Options getDefaultOptions() {
BitmapFactory.Options decodeBitmapOptions;
synchronized (OPTIONS_QUEUE) {
decodeBitmapOptions = OPTIONS_QUEUE.poll();
}
if (decodeBitmapOptions == null) {
decodeBitmapOptions = new BitmapFactory.Options();
resetOptions(decodeBitmapOptions);
}
return decodeBitmapOptions;
}
private static void resetOptions(BitmapFactory.Options decodeBitmapOptions) {
decodeBitmapOptions.inTempStorage = null;
decodeBitmapOptions.inDither = false;
decodeBitmapOptions.inScaled = false;
decodeBitmapOptions.inSampleSize = 1;
decodeBitmapOptions.inPreferredConfig = null;
decodeBitmapOptions.inJustDecodeBounds = false;
decodeBitmapOptions.inDensity = 0;
decodeBitmapOptions.inTargetDensity = 0;
decodeBitmapOptions.outWidth = 0;
decodeBitmapOptions.outHeight = 0;
decodeBitmapOptions.outMimeType = null;
decodeBitmapOptions.inBitmap = null;
decodeBitmapOptions.inMutable = true;
}
在我們解碼方法的上游decode方法中在調(diào)用decodeFromWrappedStreams之前通過getDefaultOptions()來獲取BitmapFactory.Options,然后在getDefaultOptions()方法里實(shí)例化了一個(gè)新的BitmapFactory.Options,并通過resetOptions方法進(jìn)行初始化,初始化時(shí)inPreferredConfig值為空,為了使用內(nèi)存復(fù)用機(jī)制設(shè)置了inMutable = true。之后再調(diào)用decodeFromWrappedStreams方法時(shí)傳入這個(gè)options,這就是為什么解碼尺寸時(shí),沒有inPreferredConfig的原因了。
總結(jié):整體看下來為什么會(huì)出現(xiàn)這個(gè)問題呢,Glide使用了Glide.Options和BitmapFactory.Options兩套來維護(hù)解碼方式(當(dāng)然Glide.Options還做了很多其他事情),一直到decode方法時(shí)才實(shí)例化BitmapFactory.Options。在decodeFromWrappedStreams方式解碼時(shí),先調(diào)用了解碼圖片尺寸,這時(shí)給BitmapFactory.Options賦值了ARGB-8888解碼,然后才做calculateConfig解碼方式的配置,導(dǎo)致在獲取復(fù)用內(nèi)存時(shí)獲取到了ARGB-8888解碼的內(nèi)存。
我們配置了inPreferredConfig = RGB-565解碼,復(fù)用了ARGB-8888解碼的內(nèi)存,最后我們的圖片是什么格式呢?
是RGB-565格式的,實(shí)際我們圖片內(nèi)存只有真實(shí)占用內(nèi)存的一半,但整塊內(nèi)存是無法被釋放的,不知道這算不算Glide一個(gè)bug?