3

I. Introduction

The author recently worked on the stability maintenance of the vivo game center. When analyzing online abnormalities, I found that a considerable part of it was caused by OutOfMemory. When it comes to OOM, we generally think of memory leaks. In fact, there is often another factor-pictures. If the pictures are used improperly, it is easy to eat a lot of memory and cause exceptions.

In particular, several important versions of the game center from the end of 2020 to the beginning of 2021 have launched many content-related features and introduced a large number of pictures and video lists, which has led to an increase in the proportion of online OOM.

In this article, the author will explain the memory usage of a seemingly ordinary Bitmap, introduce the tools in Android Studio that help us analyze the memory usage of images, and illustrate the two popular image loading frameworks: Glide and Picasso are loading images Different ways of using memory at the same time, then analyze the display strategy of pictures in different drawable directories, and finally put forward a scheme to optimize memory allocation based on the phone memory and version.

Second, view the picture memory usage

How much space a picture occupies in memory, a common misunderstanding is that the picture itself will take up as much memory as it is on disk/downloaded from the Internet. This statement is incorrect. The size of the memory occupied by a picture does not depend on its own size, but on the memory applied for by the display method adopted by the picture library.

Take the picture of Iron Man as an example. Its size is 350*350. You can see that it only occupies 36KB of space on the computer disk.

We create a simple Demo with an ImageView in the center of the page to display this Iron Man picture.

Perform heap dump through Android Studio to see the memory occupied by the picture. First, we save the memory snapshot when the picture is displayed. The operation path is Profiler -> Memory -> Heap Dump, which will generate a dump file in which you can see the current heap usage.

As you can see in the picture below, when the program is running, the "Iron Man" picture occupies a memory (Retained Size) of 2560000bytes, which is approximately equal to 2.4MB of memory. It is 70 times different from its 36KB size on the disk!

Tips: How to view the pictures in the dump file

When debugging, if we only have one dump file, we often need to restore the image content to help locate the problem. There are two ways to extract the original picture from the dump file.

Method 1: View

If the dump file is sourced from a device with Android version 7.1.1 (Android N, API=25) and below, this method can be used. It is very convenient to select the Bitmap object and directly view the image content in the Bitmap Preview of the window (as shown above).

Method 2: View

This method is applicable to all Android devices. First, MAT , and sometimes the following error occurs:

The reason is that the dump file generated by Android Studio's Profiler is not in a standard format. We can use the tool located in the path SDK/platform-tools/hprof-conv.exe to convert it to a standard format. The conversion command is:

hprof-conv.exe <in-file> <out-file>

Open the converted dump file through MAT, find the byte[] attribute of the Bitmap object in it, and copy it to the image01.data file.

Tip: You can see that the size of the image01.data file here is 2.44MB, which is exactly the memory occupied by the image at runtime.

Then use the GIMP tool to open the file, select RGBA in the format (most Bitmaps use this format), the width and height can be seen in the MAT, the author here is 800 * 800. After setting the format and width and height, you can see the true face of the picture.

2. Calculation formula of picture memory usage

In the previous chapter, we know that a 36KB image downloaded through the network requires 2.4MB of space when it is loaded into the memory. Next, explain the conversion relationship, let us remember a formula:

Image occupied memory = image quality * width * height

There are three factors of "image quality", "width" and "height". It involves the realization of the image loading framework. Different frameworks have different default values for these three. We take the current most popular Take Picasso and Glide as examples.

Picasso

In Picasso, the width and height of the picture displayed by default are the same as the original picture. Still taking this Iron Man as an example, the picture itself is 350px 350px, when we load it into the 200px 200px ImageView, the occupied space is 0.49MB .

Therefore, when the target ImageView is smaller than the image size, a good practice is to use an image source that does not exceed the ImageView size. On the one hand, it can shorten the image download time, and on the other hand, it can help optimize memory usage.

Glide

Glide uses a completely different approach, and the final width and height it uses is the width and height of the target ImageView. If we load the same image into a 200px * 200px ImageView, the space occupied is only 0.16MB.

makes Picasso achieve the same effect as Glide

The designers of Picasso also discovered this shortcoming and provided a series of methods to adjust the size of the final loaded image. One of them is fit(). This method can achieve the same effect as Glide.

Picasso().get().load(IMAGE_URL).fit().into(imageVIEW)

Opposite scene: the small image is loaded into the large ImageView

Usually, in order to provide a clearer interface and prevent distortion and blurring of the picture after stretching, the pictures provided by the designer are all high-resolution. The scene we are facing is to load a large picture into a small ImageView. However, the opposite possibility is not ruled out: loading the small image into the large ImageView. At this time, Glide's default memory strategy is insufficient: it uses the size of the target ImageView as the final width and height.

For example, when the Iron Man image of 600, the memory occupied is as high as 1.41MB.

600 600 4bytes = 1.41MB

Is there a way to take into account the different size relationship between the original image and the target ImageView? ——Yes, this is centerInside().

Glide.with(this).load(IMAGE_URL).centerInside().into(imageView)

With the help of the centerInside() method, the effect of "taking the minimum width and height in the original image and the target ImageView as the size of the final loaded image" can be achieved.

3. Picture quality

What is "picture quality"? Simply put, how many bytes are used to represent the color of a pixel. Its scientific name is " bit depth ", which can be seen in the image properties.

The picture bit depth usually has 1 bit, 8 bit, 16 bit, 24 bit, 32 bit.

PNG format has three forms: 8-bit, 24-bit, and 32-bit. Among them, 8-bit PNG supports two different transparency forms (indexed transparency and alpha transparency), 24-bit PNG does not support transparency, and 32-bit PNG is added on the basis of 24-bit. With an 8-bit transparent channel, it can show 256 levels of transparency.

The default picture quality used by Glide and Picasso is ARGB_8888, which is 32-bit depth with transparency. A pixel needs 4bytes of memory. This also explains why the above calculations are based on the formula of width_height_4bytes.

Note: Starting from v4, Glide will use ARGB\_8888 as the default configuration. Until then, it has been using RGB\_565 by default.

For most pictures used by the client, the display quality of 32-bit depth and 16-bit depth is difficult to distinguish with the naked eye, but the difference in memory occupied by them is exactly doubled. Therefore, the author suggests to use RGB_565 as the mode for loading pictures in most scenarios. Except for the following two scenarios:

1) Pictures with transparent parts: If the picture is displayed in the RGB_565 picture format, the transparent areas cannot be displayed normally. For example, in the Iron Man picture above, the original transparent part will be displayed as black.

2) Pictures with gradient colors and high display quality requirements: 32-bit can support more colors than 16-bit, and present a more natural transition on the gradient display (as shown in the figure below). At this time, we should make a trade-off between display quality and application performance. For low-end devices, application stability is more important than display quality. I strongly recommend using 16-bit depth to display.

4. How to load pictures in the drawable directory

In the resource directory of the project, there are generally drawable-mdpi, drawable-hdpi, drawable-xhdpi, drawable-xxhdpi, drawable-xxxhdpi directories, which are used to match devices with different display densities. The corresponding table is as follows.

The dpi of the current device can be obtained through adb shell wm density. After executing the Nexus 6P emulator, it can be read that its dpi is 560, which belongs to xxxhdpi.

$ adb shell wm densityPhysical density: 560

Then the same picture is placed in different directories, does it affect the allocation of memory? The answer is yes, based on two simple derivations:

  • The resource catalog where the picture is located and the device density determine the final pixel size of the picture displayed on the screen;
  • Pixel size and picture quality jointly determine the allocation of memory.

The second point has been explained above, and the first point is mainly analyzed here. Use picture editing software to enlarge the original Iron Man picture 700, and put them into the xhdpi and xxxhdpi directories respectively.

Why use such a combination? Because from the above table, the display density of xhdpi and xxxhdpi is 1:2, which means that when a xxxhdpi device displays pictures in the drawable-xhdpi directory, it will be enlarged to 2 times for display. So we put 350 350 bone slices into drawable-xhdpi, and 700 700 pictures into drawable-xxxhdpi, expecting that they will eventually display the same size on the screen.

Create two ImageViews in the layout and observe the final display effect of these two pictures and the allocation of memory.

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000">
​
    <!-- 350 * 350,位于drawable-xhdpi -->
    <ImageView
        android:id="@+id/iv_image_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="40dp"
        android:src="@drawable/iron_man_350_square_xhdpi"
        />
​
    <!-- 700 * 700,位于drawable-xxxhdpi -->
    <ImageView
        android:id="@+id/iv_image_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="40dp"
        android:layout_gravity="bottom"
        android:src="@drawable/iron_man_700_square_xxxhdpi"
        />
​
</FrameLayout>

The display effect and memory allocation are as follows:

The following conclusions can be drawn from the analysis:

For a picture with a display size of 613 613, it occupies a memory of 613 613 * 4 = 1,503,076B ≈ 1.5MB, which is in line with our analysis of the picture memory above;

What determines the memory occupied by a picture is its final display size on the screen, which has no direct relationship with the resolution of the picture itself and which drawable directory it is in;

Since the density of xxxhdpi is twice the density of xhdpi, on Nexus 6P devices with a screen density of xxxhdpi, the pictures in the drawable-xxxhdpi directory are displayed with approximate original pixel size (700px) (displayed as 613px) and are located in the drawable The pictures in the -xhdpi directory are enlarged to 2 times, and the final display size is also 613px.

Five, optimization strategy

In actual development, we hope that mid-to-high-end models will load clearer pictures (ARGB\_8888) to improve user experience, and for low-end models, we hope to load pictures that take up less memory (RGB\_565) to reduce The probability of OOM occurrence. This configuration can be done when Glide is initialized. It should be noted that this optimization solution should not be used for pictures with transparent areas.

@GlideModule
class MyGlideModule : AppGlideModule() {
​
    override fun applyOptions(context: Context, builder: GlideBuilder) {
        builder.setDefaultRequestOptions(RequestOptions().format(getBitmapQuality()))
    }
​
    private fun getBitmapQuality(): DecodeFormat {
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || hasLowRam()) {
            // 低端机型采用RGB_565以节约内存
            DecodeFormat.PREFER_RGB_565
        } else {
            DecodeFormat.PREFER_ARGB_8888
        }
    }
}

Six, summary

With the help of some open source tools, we can easily locate the big picture, such as Didi open source DoKit , which is not introduced in detail for reasons of space. Finally, we summarize a few suggestions for our daily development, and hope that your application stability is steadily increasing.

  • In multi-picture scenes (such as RecyclerView), pay attention to releasing picture resources in time;
  • Use a picture format that occupies a smaller memory;
  • The image source file size should be similar to the target ImageView;
  • Giving priority to meeting xxhdpi, xxxhdpi image resource requirements;
  • According to the performance of the device, different image loading strategies are adopted.
Author: vivo Internet Client Team-Li Lei

vivo互联网技术
3.3k 声望10.2k 粉丝