Detailed explanation of bitmap sampling and memory calculation of Bitmap

The original text was first published on the WeChat public account: jzman-blog, welcome to pay attention to the exchange!

A problem often considered in Android development is OOM (Out Of Memory), which is memory overflow. On the one hand, OOM may occur when a large number of images are loaded, and OOM can be avoided by sampling and compressing images. On the other hand, if a 1024 x 768 pixel The image is displayed in a 128 x 96 ImageView, which is obviously not worth it. You can load a suitable reduced version into memory by sampling to reduce memory consumption. There are two main optimizations for Bitmap. The aspects are as follows:

  1. Efficiently handle larger bitmaps
  2. cache bitmap

This article mainly focuses on how to efficiently handle larger bitmaps.

Also, what should be considered when loading a minified version into memory by bitmap sampling in Android?

  1. Estimate the memory required to load the full image
  2. The space required to load this image imposes other memory requirements on its program
  3. The dimensions of the ImageView or UI component to which the image is loaded
  4. The screen size or density of the current device

Bitmap sampling

Images come in different shapes and sizes, and reading larger images can consume memory. Reads the size and type of a bitmap. In order to create a bitmap from multiple resources, the BitmapFactory class provides many decoding methods. The most appropriate decoding method is selected according to the image data resource. These methods attempt to request the allocation of memory to construct the bitmap. , so it is easy to cause OOM exception. Each type of decoding method has additional features that allow you to specify decoding options via the BitMapFactory.Options class. When decoding, set inJustDecodeBounds to true to read the size and type of the image before allocating memory. The following code implements simple bitmap sampling:

/**
  * 位图采样
  * @param res
  * @param resId
  * @return
  */
public Bitmap decodeSampleFromResource(Resources res, int resId){
    //BitmapFactory创建设置选项
    BitmapFactory.Options options = new BitmapFactory.Options();
    //设置采样比例
    options.inSampleSize = 200;
    Bitmap bitmap = BitmapFactory.decodeResource(res,resId,options);
    return bitmap;
}
复制代码

Note : other decode... methods are similar to decodeResource, and decodeResource is used as an example here.

In actual use, the appropriate inSampleSize must be calculated according to the specific width and height requirements to sample the bitmap. For example, an image with a resolution of 2048 x 1536 is encoded with an inSampleSize value of 4 to generate a 512 x 384 image, here Assuming that the bitmap is configured as ARGB_8888, it is only 0.75M loaded into the memory instead of the original 12M. The calculation of the memory occupied by the image will be described below. The following is the calculation method for calculating the sampling ratio according to the required width and height:

/**
 * 1.计算位图采样比例
 *
 * @param option
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public int calculateSampleSize(BitmapFactory.Options option, int reqWidth, int reqHeight) {
    //获得图片的原宽高
    int width = option.outWidth;
    int height = option.outHeight;

    int inSampleSize = 1;
    if (width > reqWidth || height > reqHeight) {
        if (width > height) {
            inSampleSize = Math.round((float) height / (float) reqHeight);
        } else {
            inSampleSize = Math.round((float) width / (float) reqWidth);
        }
    }
    return inSampleSize;
}

/**
 * 2.计算位图采样比例
 * @param options
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public int calculateSampleSize1(BitmapFactory.Options options, int reqWidth, int reqHeight) {

    //获得图片的原宽高
    int height = options.outHeight;
    int width = options.outWidth;

    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
        // 计算出实际宽高和目标宽高的比率
        final int heightRatio = Math.round((float) height / (float) reqHeight);
        final int widthRatio = Math.round((float) width / (float) reqWidth);
        /**
         * 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
         * 一定都会大于等于目标的宽和高。
         */
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
    }
    return inSampleSize;
}

复制代码

After obtaining the sampling ratio, you can process larger images according to the required width and height. The following is the inSampleSize calculated according to the required width and height to sample the larger bitmap:

/**
 * 位图采样
 * @param resources
 * @param resId
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public Bitmap decodeSampleFromBitmap(Resources resources, int resId, int reqWidth, int reqHeight) {
    //创建一个位图工厂的设置选项
    BitmapFactory.Options options = new BitmapFactory.Options();
    //设置该属性为true,解码时只能获取width、height、mimeType
    options.inJustDecodeBounds = true;
    //解码
    BitmapFactory.decodeResource(resources, resId, options);
    //计算采样比例
    int inSampleSize = options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight);
    //设置该属性为false,实现真正解码
    options.inJustDecodeBounds = false;
    //解码
    Bitmap bitmap = BitmapFactory.decodeResource(resources, resId, options);
    return bitmap;
}
复制代码

The BitmapFactory.decodeResource() method is used in the decoding process, as follows:

/**
 * 解码指定id的资源文件
 */
public static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options opts) {
    ...
    /**
     * 根据指定的id打开数据流读取资源,同时为TypeValue进行复制获取原始资源的density等信息
     * 如果图片在drawable-xxhdpi,那么density为480dpi
     */
    is = res.openRawResource(id, value);
    //从输入流解码出一个Bitmap对象,以便根据opts缩放相应的位图
    bm = decodeResourceStream(res, value, is, null, opts);
    ...
}
复制代码

Obviously the real decoding method should be the decodeResourceStream() method, as follows:

/**
 * 从输入流中解码出一个Bitmap,并对该Bitmap进行相应的缩放
 */
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
                     InputStream is, Rect pad, BitmapFactory.Options opts) {

    if (opts == null) {
        //创建一个默认的Option对象
        opts = new BitmapFactory.Options();
    }

    /**
     * 如果设置了inDensity的值,则按照设置的inDensity来计算
     * 否则将资源文件夹所表示的density设置inDensity
     */
    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    /**
     * 同理,也可以通过BitmapFactory.Option对象设置inTargetDensity
     * inTargetDensity 表示densityDpi,也就是手机的density
     * 使用DisplayMetrics对象.densityDpi获得
     */
    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }
    //decodeStream()方法中调用了native方法
    return decodeStream(is, pad, opts);
}
复制代码

After setting inDensity and inTargetDensity, the decodeStream() method is called, which returns the fully decoded Bitmap object, as follows:

/**
 * 返回解码后的Bitmap,
 */
public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
    ...
    bm = nativeDecodeAsset(asset, outPadding, opts);
    //调用了native方法:nativeDecodeStream(is, tempStorage, outPadding, opts);
    bm = decodeStreamInternal(is, outPadding, opts);
    Set the newly decoded bitmap's density based on the Options
    //根据Options设置最新解码的Bitmap
    setDensityFromOptions(bm, opts);
    ...
    return bm;
}
复制代码

Obviously, the decodeStream() method mainly calls the local method to complete the decoding of the Bitmap. Tracking the source code, it is found that the nativeDecodeAsset() and nativeDecodeStream() methods both call the dodecode() method. The key code of the doDecode method is as follows:

/**
 * BitmapFactory.cpp 源码
 */
static jobject doDecode(JNIEnv*env, SkStreamRewindable*stream, jobject padding, jobject options) {
    ...
    if (env -> GetBooleanField(options, gOptions_scaledFieldID)) {
        const int density = env -> GetIntField(options, gOptions_densityFieldID);
        const int targetDensity = env -> GetIntField(options, gOptions_targetDensityFieldID);
        const int screenDensity = env -> GetIntField(options, gOptions_screenDensityFieldID);
        if (density != 0 && targetDensity != 0 && density != screenDensity) {
            //计算缩放比例
            scale = (float) targetDensity / density;
        }
    }
    ...
    //原始Bitmap
    SkBitmap decodingBitmap;
    ...

    //原始位图的宽高
    int scaledWidth = decodingBitmap.width();
    int scaledHeight = decodingBitmap.height();

    //综合density和targetDensity计算最终宽高
    if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
        scaledWidth = int(scaledWidth * scale + 0.5f);
        scaledHeight = int(scaledHeight * scale + 0.5f);
    }
    ...
    //x、y方向上的缩放比例,大概与scale相等
    const float sx = scaledWidth / float(decodingBitmap.width());
    const float sy = scaledHeight / float(decodingBitmap.height());
    ...
    //将canvas放大scale,然后绘制Bitmap
    SkCanvas canvas (outputBitmap);
    canvas.scale(sx, sy);
    canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, & paint);
}

复制代码

The above code can see the calculation of the scaling ratio, as well as the influence of density and targetDensity on the width and height of the Bitmap, which actually indirectly affects the size of the memory occupied by the Bitmap. This problem will be explained in the following example. Pay attention to the resources corresponding to density and the current Bitmap. The directory of the file (picture) is related. If a picture is located in the drawable-xxhdpi directory, the density of the corresponding Bitmap is 480dpi, and the targetDensity is the densityDpi of DisPlayMetric, which is the density represented by the mobile phone screen. So how to check the implementation of the native method in Android? The link is as follows: BitmapFactory.cpp , you can directly search for the method name of the native method.

Bitmap memory calculation

First, contribute a large picture of 6000 x 4000, the picture is close to 12M, [can be requested on the public account zero point small building] When loading this picture directly into the memory, OOM will definitely occur, of course, through appropriate bitmap sampling to reduce the picture can avoid OOM, Then how to calculate the memory occupied by Bitmap, in general, it is calculated like this:

Bitmap Memory = widthPix * heightPix * 4
复制代码

You can use bitmap.getConfig() to get the Bitmap format, here is ARGB_8888, the next pixel in this Bitmap format occupies 4 bytes, so x 4 is required. If the picture is placed in the Android resource folder, the calculation method is as follows :

scale = targetDensity / density
widthPix = originalWidth * scale
heightPix = orignalHeight * scale
Bitmap Memory = widthPix * scale * heightPix * scale * 4
复制代码

The above briefly summarizes the calculation method of the memory occupied by Bitmap. During verification, the following method can be used to obtain the memory size occupied by Bitmap:

BitmapMemory = bitmap.getByteCount()
复制代码

Since the selected image is directly loaded, it will cause OOM, so in the following cases, sampling and compression are performed first, and then the memory occupied by Bitmap is calculated.

direct sampling

This method is to directly specify the value of the sampling ratio inSampleSize, then first sample and then calculate the sampled memory, where inSampleSize is specified as 200.

  1. Put the picture in the drawable-xxhdpi directory, the density represented by drawable-xxhdpi is 480 (density), and the density represented by my mobile phone screen is 480 (targetDensity). Obviously, the scale is 1 at this time. Of course, first of all Sampling the picture, and then loading the picture into the memory, at this time, the memory occupied by the Bitmap is:
inSampleSize = 200
scale = targetDensity / density} = 480 / 480 = 1
widthPix = orignalScale * scale = 6000 / 200 * 1 = 30 
heightPix = orignalHeight * scale = 4000 / 200 * 1 = 20
Bitmap Memory =  widthPix * heightPix * 4 = 30 * 20 * 4 = 2400(Byte)
复制代码
  1. Put the picture in the drawable-xhdpi directory, the density represented by drawable-xhdpi is 320, the density represented by my mobile phone screen is 480 (targetDensity), load the picture into the memory, at this time the memory represented by Bitmap for:
inSampleSize = 200
scale = targetDensity / density = 480 / 320
widthPix = orignalWidth * scale = 6000 / 200 * scale = 45
heightPix = orignalHeight * scale = 4000 / 200 * 480 / 320 = 30
Bitmap Memory =  widthPix * scale * heightPix * scale * 4 = 45 * 30 * 4 = 5400(Byte) 
复制代码

Computational sampling

This method is to calculate the appropriate inSampleSize according to the requested width and height, rather than arbitrarily specifying the inSampleSize. This method is the most commonly used in actual development. Here, the requested width and height are 100x100. The specific inSampleSize calculation has been explained above.

  1. Put the picture in the drawable-xxhdpi directory, the density represented by drawable-xxhdpi is 480, the density represented by the screen of my mobile phone is 480 (targetDensity), load the picture into the memory, at this time the memory represented by Bitmap for:
inSampleSize = 4000 / 100 = 40
scale = targetDensity / density = 480 / 480 = 1
widthPix = orignalWidth * scale = 6000 / 40 * 1 = 150      
heightPix = orignalHeight * scale = 4000 / 40 * 1 = 100
BitmapMemory = widthPix * scale * heightPix * scale * 4 = 60000(Byte)
复制代码
  1. Put the picture in the drawable-xhdpi directory, the density represented by drawable-xhdpi is 320, the density represented by my mobile phone screen is 480 (targetDensity), load the picture into the memory, at this time the memory represented by Bitmap for:
inSampleSize = 4000 / 100 = 40
scale = targetDensity / density = 480 / 320
widthPix = orignalWidth * scale = 6000 / 40 * scale = 225
heightPix = orignalHeight * scale = 4000 / 40 * scale = 150
BitmapMemory = widthPix * heightPix * 4 = 225 * 150 * 4 = 135000(Byte)
复制代码

The calculation process of bitmap sampling and the memory occupied by Bitmap in different situations is as described above.

Test effect

The test effect diagram is referenced as follows:

drawable-xhdpi drawable-xxhdpi

If you are interested, you can follow the public account: jzman-blog to communicate and learn together.

Reprinted in: https://juejin.im/post/5d0a561af265da1b7638a67d

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324123702&siteId=291194637