图片大小和内存占用计算

作者: juexingzhe | 来源:发表于2020-03-02 11:39 被阅读0次

    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
    

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

    相关文章

      网友评论

        本文标题:图片大小和内存占用计算

        本文链接:https://www.haomeiwen.com/subject/pbupkhtx.html