图片大小和内存占用计算

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=trueadjustViewBounds=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作为targetWidhttargetHeight:

// DownsampleStrategy.java
getScaleFactor.png

这里就得到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)

expectedSize.png

看下到目前为止,BitmapFactory.Options的关键系数:

BitmapFactory_Options.png
outConfig.png

所以缩放系数是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;
densityDpi.png

那么问题来了,图片占用内存是根据原始图片还是根据缩放后的尺寸进行计算,按逻辑肯定是按照显示的尺寸来计算,进行看下没有加载图片和加载,上面的图片是没有加载图片的内存分布:

memory_before.png

下面这张是加载单张图片的内存分布:

memory_after.png

图片占用内存:

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

看下内存分布图,在加载图片之前:

widthMatchParentMemoryBefore.png

加载图片之后:

widthMatchParentMemoryAfter.png

所以图片占用内存:

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

综上所述,在展示图片的时候要尽量提供具体的尺寸,如果没有提供尺寸而是用包裹内容的配置很有可能会导致图片解码出来比实际需要的占用更大内存。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容