Android中图片是内存大户,不小心的话就会导致内存浪费,严重的会引起OOM。Android里面展示图片的控件ImageView可以使用BitmapDrawable或者Bitmap来展示图片,那么ImageView的大小和展示图片的大小是一一对应的吗?显示图片需要的内存怎么计算呢,是根据ImageView的大小还是图片的大小进行计算?
实践出真知,通过一个Demo来看下。
首先准备一张图片,尺寸是1080*620,这里就不展示出来了,占用篇幅也没意义。
通过下面的代码获取下手机本身的一些尺寸参数:
Log.w(TAG, "xdpi = " + getResources().getDisplayMetrics().xdpi);
Log.w(TAG, "ydpi = " + getResources().getDisplayMetrics().ydpi);
Log.w(TAG, "widthPixel = " + getResources().getDisplayMetrics().widthPixels);
Log.w(TAG, "heightPixels = " + getResources().getDisplayMetrics().heightPixels);
Log.w(TAG, "density = " + getResources().getDisplayMetrics().density);
Log.w(TAG, "densityDpi = " + getResources().getDisplayMetrics().densityDpi);
Log.w(TAG, "scaledDensity = " + getResources().getDisplayMetrics().scaledDensity);
===out===
xdpi = 400.315
ydpi = 410.849
widthPixel = 1080
heightPixels = 2214
density = 3.0
densityDpi = 480
scaledDensity = 3.0
关于屏幕密度和density的计算参考:
Android drawable微技巧,你所不知道的drawable的那些细节
在xml中设置ImageView的参数,分成两种情况,分别设置adjustViewBounds=true
和adjustViewBounds=false
,分别得到ImageView的宽高:
1. ImageView.layoutParams = WrapContent
`adjustViewBounds=false`
===out===
ImageView.width = 1080
ImageView.height = 1271
2. ImageView.layoutParams = WrapContent
`adjustViewBounds=true`
===out===
ImageView.width = 1080
ImageView.height = 620
How to get ImageView size?
第一次和第二次会获取不到大小,这时候图片大小未确定
imageView.getViewTreeObserver().addOnPreDrawListener{
Log.w(TAG, "===width, " + imageView.getWidth());
Log.w(TAG, "===height, " + imageView.getHeight());
}
// out
GlideWidth = 2214, GlideHeight = 2214
===width, 0
===height, 0
===width, 0
===height, 0
===width, 1080
===height, 620
Glide下载图片之前需要确定ImageView的大小才会开始下载,而因为ImageView是WrapContent,所以大小怎么确定?通过下面方式测试,并且该log会在上面ImageView之前打印:
into(new VCustomViewTarget(imageView, new VSizeReadyCallback() {
@Override
public void onSizeReady(int width, int height) {
Log.i(TAG, "GlideWidth = " + width + ", GlideHeight = " + height);
}
})
);
// out
ImageLoaderTag: GlideWidth = 2214, GlideHeight = 2214
那上面得到的2214这个数值根据什么逻辑?,注释里面可以看到,Glide会把WrapContent当成获取屏幕大小,如果需要加载图片原始大小,那么通过.override(Target.SIZE_ORIGINAL)
。所以这里拿到的是Math.max(1080, 2214)
。
// ViewTarget.java
private int getTargetDimen(int viewSize, int paramSize, int paddingSize) {
if (!view.isLayoutRequested() && paramSize == LayoutParams.WRAP_CONTENT) {
if (Log.isLoggable(TAG, Log.INFO)) {
Log.i(TAG, "Glide treats LayoutParams.WRAP_CONTENT as a request for an image the size of"
+ " this device's screen dimensions. If you want to load the original image and are"
+ " ok with the corresponding memory cost and OOMs (depending on the input size), use"
+ " .override(Target.SIZE_ORIGINAL). Otherwise, use LayoutParams.MATCH_PARENT, set"
+ " layout_width and layout_height to fixed dimension, or use .override() with fixed"
+ " dimensions.");
}
return getMaxDisplayLength(view.getContext());
}
return PENDING_SIZE;
}
private static int getMaxDisplayLength(@NonNull Context context) {
if (maxDisplayLength == null) {
WindowManager windowManager =
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = Preconditions.checkNotNull(windowManager).getDefaultDisplay();
Point displayDimensions = new Point();
display.getSize(displayDimensions);
maxDisplayLength = Math.max(displayDimensions.x, displayDimensions.y);
}
return maxDisplayLength;
}
并且Glide把获取到的2214传到后面decode作为targetWidht
和targetHeight
:
// DownsampleStrategy.java
这里就得到ScaleFactor = 2.05
最终decode出来的原始Bitmap会乘以这个系数进行拉伸,看下几个打印参数, 在inDensity为0时会在后面解码缩放后进行设置(具体见后面分析),inTargetDensity为0时会默认根据目标显示设备密度(这里是480)。这里是根据原始文件File做解码得到的,尺寸和原始尺寸一样,原图占用内存大小就是:1080 * 620 * 4 = 2678400
// imageView.setAdjustViewBounds(true);
imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
Log.w(TAG, "===width, " + imageView.getWidth());
Log.w(TAG, "===height, " + imageView.getHeight());
BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable();
if (drawable == null) return true;
Log.w(TAG, "===instrinsicWidth, " + drawable.getIntrinsicWidth());
Log.w(TAG, "===intrinsicHeight, " + drawable.getIntrinsicHeight());
Bitmap bitmap = drawable.getBitmap();
if (bitmap == null) return true;
Log.w(TAG, "===bitmapDensity, " + bitmap.getDensity());
Log.w(TAG, "===bitmapWidth, " + bitmap.getWidth());
Log.w(TAG, "===bitmapHeight, " + bitmap.getHeight());
Log.w(TAG, "===bitmapMemory, " + bitmap2.getAllocationByteCount());
return true;
}
});
// out
===width, 1080
===height, 1271
===instrinsicWidth, 2214
===intrinsicHeight, 1271
===bitmapDensity, 480
===bitmapWidth, 2214
===bitmapHeight, 1271
===bitmapMemory, 2678400
再看imageView.setAdjustViewBounds(true);
,还记得上面的scaleFactor吗?
其中instrinsicWidth=1080* 2.05; intrinsicHeight = 620 * 2.05
,同理从ImageView拿到的Bitmap的宽高也是(1080*2.05, 620 * 2.05),根据原图的大小进行缩放了。
// imageView.setAdjustViewBounds(true);
代码同上
// out
===width, 1080
===height, 620
===instrinsicWidth, 2214
===intrinsicHeight, 1271
===bitmapDensity, 480
===bitmapWidth, 2214
===bitmapHeight, 1271
===bitmapMemory, 2678400
可以看出如果imageView.setAdjustViewBounds(false);
,ImageView.height
宽度变成了1271,不再是原始尺寸的620
.其他占用参数尺寸则是完全一样,包括内存占用情况。
源码面前,以前毫无遮掩,看下解码的逻辑,如果调用的时候设置Target.SIZE_ORIGINAL
那么就会得到图片的原始宽高,否则就会根据上面的缩放系数进行计算得到(2214, 1271)
// Downsampler.java
private Bitmap decodeFromWrappedStreams(InputStream is,
BitmapFactory.Options options, DownsampleStrategy downsampleStrategy,
DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth,
int requestedHeight, boolean fixBitmapToRequestedDimensions,
DecodeCallbacks callbacks) throws IOException {
...
int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;
ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
...
}
接着上面逻辑往下走计算采样系数(这里因为图片原始宽高小于需要显示的宽高(2214/2214),所以不会进行采样),然后根据采样宽高计算期望的Bitmap宽高尺寸:(2214, 1271)
看下到目前为止,BitmapFactory.Options
的关键系数:
所以缩放系数是inTargetDensity/inDensity = 2.05
再接着往下就会根据期望的参数配置去BitmapPool
获取Bitmap
:
// Downsampler.java-decodeFromWrappedStreams
setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);
// Downsampler.java
@TargetApi(Build.VERSION_CODES.O)
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;
}
expectedConfig = options.outConfig;
}
if (expectedConfig == null) {
expectedConfig = options.inPreferredConfig;
}
// BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
}
最后就是进行decode, 解码成功后会设置解码后的Bitmap的Density = displayMetrics.densityDpi
,这里就是480,需要展示的图片已经根据缩放系数(这里是2.05)进行缩放,所以返回回去后计算内存宽高就不需要进行缩放计算得到内存占用大小。再强调一下这里输出的Bitmap尺寸是(2214, 1271)
// Downsampler.java-decodeFromWrappedStreams
Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
callbacks.onDecodeComplete(bitmapPool, downsampled);
Bitmap rotated = null;
if (downsampled != null) {
// If we scaled, the Bitmap density will be our inTargetDensity. Here we correct it back to
// the expected density dpi.
downsampled.setDensity(displayMetrics.densityDpi);
rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation);
if (!downsampled.equals(rotated)) {
bitmapPool.put(downsampled);
}
}
return rotated;
那么问题来了,图片占用内存是根据原始图片还是根据缩放后的尺寸进行计算,按逻辑肯定是按照显示的尺寸来计算,进行看下没有加载图片和加载,上面的图片是没有加载图片的内存分布:
下面这张是加载单张图片的内存分布:
图片占用内存:
16MB - 4.9MB = 11.1MB
而根据上面instrinsicWidth和intrinsicHeight:
2214 * 1271 * 4 = 10.7MB
所以是根据屏幕显示的尺寸,也就是缩放后的尺寸进行计算。
重点来了,其实ImageView.size = (1080,620)也就是实际显示只需要内存:
1080 * 620 * 4 = 2.55MB
但是现在确实10.7MB,多了接近8MB,我们设置ImageView.params(matchparent, wrapcontent)
:实际在屏幕上显示效果是一致的,上面的宽度是2214已经明显超出屏幕宽度:
xdpi = 400.315
ydpi = 410.849
widthPixel = 1080
heightPixels = 2214
density = 3.0
densityDpi = 480
scaledDensity = 3.0
GlideWidth = 1080, GlideHeight = 2214
===width, 1080
===height, 0
===width, 1080
===height, 0
i = 9
===width, 1080
===height, 620
===instrinsicWidth, 1080
===intrinsicHeight, 620
===bitmapDensity, 480
===bitmapWidth, 1080
===bitmapHeight, 620
===GlideMimeType, image/jpeg
===GlideOutWidth, 1080
===GlideOutHeight, 620
===GlideBitmapDensity, 0
===GlideBitmapTargetDensity, 0
===GlideBitmapWidth, 1080
===GlideBitmapHeight, 620
===GlideBitmapMemory, 2678400
计算内存占用:
1080 * 620 * 4 = 2.55MB
看下内存分布图,在加载图片之前:
加载图片之后:
所以图片占用内存:
5MB - 2.4MB = 2.6MB
综上所述,在占用内存方面,同样的显示效果,如果原图尺寸比需要显示的尺寸(比如设置ImageView WrapContent)小,并且没有设置Target_Origin_Size的话,wrap_content
会比match_parent
的情况多很多的内存占用,这是因为计算逻辑是根据屏幕最大尺寸MAX_SIZE = Math.Max(Screen_Width, Screen_Height),再分别除以图片的原始尺寸大小得到一个最小系数Math.Min(MAX_SIZE/Original_Width, MAX_SIZE/Original_Height),然后在将原始尺寸乘以该系数得到缩放后的尺寸,也就是最后显示图片。
在这个图片情况(98KB)下,多出了8M左右的内存占用。
看下另外一种情况,原图尺寸比需要显示的尺寸大(比如设置ImageView.width=200, height=200),通过如下代码打印一些参数,原图尺寸还是(1080*620),ImageView尺寸是(600 * 600),所以系数a=Math.min(600/1080, 600/620)=0.556,可以得到缩放后展示的大小(0.556 * 1080, 0.556 * 620) = (600, 344)。
那么缩放后占用内存大小Memory=600 * 344 * 4(RGB_8888)= 825600 < 2678400(原图大小)
Log.w(TAG, "===width, " + imageView.getWidth());
Log.w(TAG, "===height, " + imageView.getHeight());
BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable();
if (drawable == null) return true;
// get image scaled size
Log.w(TAG, "===instrinsicWidth, " + drawable.getIntrinsicWidth());
Log.w(TAG, "===intrinsicHeight, " + drawable.getIntrinsicHeight());
Bitmap bitmap = drawable.getBitmap();
Log.w(TAG, "===bitmapShowMemory, " + bitmap.getAllocationByteCount());
Log.w(TAG, "===bitmapShowMemory, " + bitmap.getByteCount());
//get image original size
Log.w(TAG, "===bitmapDensity, " + bitmap.getDensity());
Log.w(TAG, "===bitmapWidth, " + bitmap.getWidth());
Log.w(TAG, "===bitmapHeight, " + bitmap.getHeight());
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = false;
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.glidepng, o);
// get drawable directory density for mdpi 160 xxhdpi 480
Log.w(TAG, "===bitmapDensity, " + o.inDensity);
// phone screen density
Log.w(TAG, "===bitmapTargetDensity, " + o.inTargetDensity);
// origin size * inTargetDensity / inDensity
Log.w(TAG, "===bitmapOutWidth, " + bitmap2.getWidth());
Log.w(TAG, "===bitmapOutHeight, " + bitmap2.getHeight());
Log.w(TAG, "===bitmapMemory, " + bitmap2.getAllocationByteCount()
===out===
===width, 600
===height, 600
===instrinsicWidth, 600
===intrinsicHeight, 344
===bitmapRealMemory, 825600
===bitmapShowMemory, 825600
===bitmapDensity, 480
===bitmapWidth, 600
===bitmapHeight, 344
===bitmapDensity, 480
===bitmapTargetDensity, 480
===bitmapOutWidth, 1080
===bitmapOutHeight, 620
===bitmapMemory, 2678400
综上所述,在展示图片的时候要尽量提供具体的尺寸,如果没有提供尺寸而是用包裹内容的配置很有可能会导致图片解码出来比实际需要的占用更大内存。