Note: this article is a little longer, it is recommended to pay attention to and collect.

One picture, ruin ten

There is a saying in Android Mobile Performance: “One picture can destroy ten.” This means that the resident memory of one image will result in ten optimizations in vain.

Why do you say that? Let’s take a test.

I first prepared an 800*450 JPG image, about 49.6KB in size, under the res/ Drawable folder of the project:

And loads it the size of a dp 400 * 200 dp ImageView, using API is BitmapFactory decodeResource () :

    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dijia);
    imageView.setImageBitmap(bitmap);
Copy the code

Let’s look at the memory changes before and after the image is displayed:

As you can see, this tiny image, displayed in a tiny ImageView, eats up 12.9 MB of Java memory!

We then obtain the bitmap size through the API provided by the system:

    bitmap.getByteCount();
Copy the code

The BITmap size obtained through the API is 12.96MB, which is consistent with the Java memory growth, indicating that the sharp increase in Java memory consumption was indeed caused by this image.

Imagine if this image had a memory leak, it would be a nightmare.

We know that a simple Activity interface leaks memory, usually between ten KB and 1MB; Image memory leaks can be tens of kilobytes or even tens of megabytes.

This is called “one picture, ruin ten”.

So, how does a small image eat up so much memory? What are the factors that affect how much memory you eat?

How much memory to eat

It is not hard to imagine that the size of image memory consumption, will be related to a variety of factors. We use the “control variables” method to run the test program to see what happens.

Bitmap pixel format

There are several common bitmap pixel formats:

  • RGB_565: Each pixel uses 2 bytes, only RGB channels are decoded — R channel 5 bits, G channel 6 bits, B channel 5 bits, total 16 bits;

  • ARGB_8888: uses 4 bytes per pixel, ARGB 4 channels 8 bits per channel, total 32 bits;

  • ARGB_4444: Poor quality, deprecated by Android official, official suggested to replace ARGB_8888;

  • ALPHA_8: Only channel A (transparent channel);

  • HARDWARE: New in Android 8.0, bitmaps are stored in Graphic Memory. This will be covered later in this article.

Let’s focus on RGB_565 and ARGB_8888. There are three important differences between them:

  • 1. RGB_565 uses 2 bytes per pixel, ARGB_8888 uses 4 bytes per pixel, and the memory space occupied is 2 times different.

  • 2. RGB_565 does not have alpha and does not support transparency, while ARGB_8888 does;

  • 3, RGB_565 RGB 3 channels occupy 5~6 bits, ARGB_8888 RGB 3 channels occupy 8 bits, the more bits, the better the color effect.

In the first test, we controlled the phone model (OPPO R9S) and the resource folder position (RES /drawable) of the image, and changed the pixel format of the bitmap only:

    BitmapFactory.Options options = new BitmapFactory.Options();
    // The default is ARGB_8888, which can be changed to RGB_565
    // options.inPreferredConfig = Bitmap.Config.RGB_565;
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dijia, options);
    imageView.setImageBitmap(bitmap);
Copy the code

Check out the test results:

As you can see, ARGB_8888 takes up 12.96MB of memory, while RGB_565 takes up 6.48MB, which is a 2x difference.

Resource Folder Directory

Different resource folder directories have different display density. The combination of display density and device resolution density finally determines the zoom ratio of the image width and height direction.

Take a look at the mapping between resource folder directories and display density:

Directory name Display density (densityDpi) note
res/drawable 160 In accordance with res/mipmap – mdpi
res/mipmap-ldpi 120
res/mipmap-mdpi 160
res/mipmap-hdpi 240
res/mipmap-xhdpi 320
res/mipmap-xxhdpi 480
res/mipmap-xxxhdpi 640

In the second test, control the phone model (OPPO R9S) and pixel format (ARGB_8888) unchanged, only change the resource folder location of the image:

As you can see, when placed in different resource folders, the bitmap’s width and height change, and the memory footprint changes accordingly.

Device resolution

The resolution of the device on which the APP is running also affects the width and height of the bitmap.

For the third test, the pixel format (ARGB_8888) and the location of the resource folder (RES/MIpmap-MDPI) remain unchanged, and only the mobile phone model used for the test is changed:

As you can see, the screen density of the two phones is different, as well as the width, height and memory footprint of the bitmap.

The test results

From these tests, the first thing you can get is this simple formula:

Bitmap Memory ≈ Total size of pixel data = Bitmap width x Bitmap height x size of bytes occupied by each pixel

However, we found that the width and resolution of the bitmap was inconsistent with that of the original resource map, which was determined by the resource folder directory resolution and the device resolution together, and there was a simple formula:

Bitmap width = Original width x (device resolution/Resource directory resolution)

So, we can get the formula for bitmap memory usage:

Bitmap Memory occupied ≈ Total size of pixel data = Width of original image x height of original image x (device resolution/resource directory resolution)^2 x size of bytes occupied by each pixel

The bitmap memory size calculated by this formula is consistent with the bitmap memory size obtained by system API bitmap.getBytecount ().

Through these tests, we know that the memory footprint of bitmap is inversely proportional to the resource directory resolution and directly proportional to other factors.

So how can you reduce the memory footprint of bitmaps?

Maybe you could eat less memory

To make Bitmaps eat less memory, we can do the following.

Bitmap pixel format

As mentioned earlier, each pixel occupies a different size of byte for different pixel formats, and RGB_565 uses half as much memory as ARGB_8888.

RGB_565 pixel format can be used if the image quality is not high in the place where the image is used.

For example, when we want to use thumbnails, blur images, etc.

Resource Folder Directory

From the above experiment and the formula, we can see that the image in the high resolution resources folder, more memory saving.

However, if you place a small image in a high resolution resource folder, it will be stretched and distorted when loading.

So, if the size of the APK package allows, the same image should be provided in as many resolutions as possible to be placed in the resource folder of the corresponding resolution, especially the images required by the higher resolution resource folder.

Sampling compression

Sampling compression is the extraction of some pixels from the total pixels of the bitmap for display. With fewer pixels, the bitmap footprint is naturally lower.

So when can you sample compression?

In simple terms, when the size of the original image exceeds the size of the target area to be displayed, the sample can be compressed.

There is an attribute inSampleSize in the BitmapFactory.Options class that controls the sampling rate:

/** * If set to a value > 1, requests the decoder to subsample the original * image, returning a smaller image to save memory. The sample size is * the number of pixels in either dimension that correspond to a single * pixel in the decoded bitmap. For example, inSampleSize == 4 returns * an image that is 1/4 the width/height of the original, and 1/16 the * number of pixels. Any value <= 1 is treated the same as 1. Note: the * decoder uses a final value based on powers of 2, any other value will * be rounded down to the nearest power of 2. */
    public int inSampleSize;
Copy the code

Here’s a quick translation of the note:

  • 1. If inSampleSize is greater than 1, the original image is sampled and compressed to save memory.

    For example, if inSampleSize == 4, then 1/4 of the horizontal and vertical pixels are sampled, and the final number of pixels is 1/16 of the original number.

  • The value of inSampleSize must be a power of 2. If it is not a power of 2, it will be evaluated downward to the nearest power of 2.

So, the focus of our sampling compression is to calculate the sampling rate inSampleSize.

    /** * Calculate the sampling rate **@param options   bitmap options
     * @paramMaxWidth Maximum width of the target area (px) *@paramMaxHeight Maximum height of the target area (px) *@return the sample size
     */
    public static int calculateInSampleSize(final BitmapFactory.Options options,
                                             final int maxWidth,
                                             final int maxHeight) {
        // bitmap the number of original vertical pixels
        int height = options.outHeight;
        // bitmap the number of pixels in the original landscape
        int width = options.outWidth;
        int inSampleSize = 1;
        while (height > maxHeight || width > maxWidth) {
            height >>= 1;
            width >>= 1;
            inSampleSize <<= 1;
        }
        return inSampleSize;
    }
Copy the code

After calculating the sampling rate, the bitmap can be sampled and compressed to obtain the compressed small bitmap.

    /** * Compress the original bitmap sample **@param res       The resources object containing the image data
     * @param resId     The resource id of the image data
     * @paramMaxWidth Maximum width of the target area (px) *@paramMaxHeight Maximum height of the target area (px) *@returnSample compressed bitmap */
    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int maxWidth, int maxHeight) {

        BitmapFactory.Options options = new BitmapFactory.Options();
        // Require the decoder to take only bitmap boundaries, not the pixels in them.
        options.inJustDecodeBounds = true;
        // Decode to get the original size
        BitmapFactory.decodeResource(res, resId, options);

        // Calculate the sampling compression rate according to the original image size and the target area size
        options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);

        // Decode the image according to the calculated inSampleSize to generate the final bitmap
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
Copy the code

The width and height of the input parameter in the above method is px. There is a certain proportion relationship between DP and PX, and the proportion relationship is related to the density of the equipment.

densityDpi 160 240 320 480 560 640
density 1 1.5 2 3 3.5 4
/**
 * Value of dp to value of px.
 *
 * @param dpValue The value of dp.
 * @return value of px
 */
public static int dp2px(Context context, final float dpValue) {
    final float scale =
            context.getResources().getDisplayMetrics().density;
    return (int) (dpValue * scale + 0.5 f);
}
Copy the code

To test the effect, I set the width of the ImageView to 200dp * 100dp and calculated the inSampleSize value to 2. Then the bitmap will be sampled and compressed to 1/4 of the original size.

HARDWARE

HARDWARE, on Android 8.0 and above, is a good way to address bitmaps that consume a lot of Java Memory because it moves pixel data from Java Memory to Graphic Memory.

I tested it on the mi MIX2 (Android 9.0), and here is the memory change before and after the image display:

As you can see, Java Memory remained unchanged at 7.5 MB before and after the image display, and consumption of Graphics Memory and Native Memory skyrocketed.

It should also be noted that HARDWARE is not strictly a pixel format. It represents the location where bitmap pixel data is stored. The internal pixel format used by HARDWARE is ARGB_8888.

PNG? JPG?

Some people may think that by compressing PNG images into JPG images, you can also save memory, right?

NO! NO! NO!

The image format is PNG or JPG, which describes the format stored in the file system and saves space in the file system.

In other words, replacing PNG with JPG may reduce the size of the APK package without reducing the Java memory footprint used to display images.

As long as they have the same resolution, the Java memory footprint for display is the same.

If you are interested, you can test it by yourself. The test chart will not be posted here.

conclusion

Bitmap is a big memory user in Android. Today we first tested how much memory bitmap eats in different situations, and analyzed how to make it eat less memory, so that it can be easily handled. In the future, we will not be afraid of bitmap problems.

In addition, you may notice that the title of this article has a prefix “OfferKiller”, OfferKiller is the learning group we organized, I in the learning group phase of the task is “Glide big picture loading, caching and progress display”, this article is a front stop.

If you are interested in OfferKiller learning group or would like to join, please follow my official account and reply to “OfferKiller” for more details. Welcome!