Android performance optimization-Bitmap memory management and long image loading

1. How to calculate the memory occupied by Bitmap?
2. Bitmap's in-memory cache management
3. Points to note when loading long images

Bitmap is the "big user" of memory usage in App. How to use Bitmap better and reduce its use of App memory is an unavoidable problem in our development.

How to get the bitmap object?

Bitmap is one of the most important categories in image processing in the Android system. Bitmap can obtain image file information, perform operations such as cutting, rotating, scaling, and compressing the image, and can save the image file in a specified format.

There are two ways to create a Bitmap object. They are created by Bitmap.createBitmap() and BitmapFactory's decode series of static methods.

Below we mainly introduce BitmapFactory's decode method to create Bitmap objects.

  • decodeFile() loads from the file system
    • Open local pictures or photos via Intent
    • Get the path of the picture according to the uri
    • Parse Bitmap according to the path: Bitmap bm = BitmapFactory.decodeFile(path);
  • decodeResource() is loaded from local resources in the form of R.drawable.xxx
    • Bitmap bm = BitmapFactory.decodeResource(getResources(),R.drawable.icon);
  • decodeStream() load from input stream
    • Bitmap bm = BitmapFactory.decodeStream(stream);
  • decodeByteArray() loads from byte array
    • Bitmap bm = BitmapFactory.decodeByteArray(myByte,0,myByte.length);

BitmapFactory.Options

  • inSampleSize: sampling rate, this is the sample size. Used to shrink the image and load it out, so as not to occupy too much memory, suitable for thumbnails.

  • inJustDecodeBounds: When inJustDecodeBounds is true, when the decodexxx method is executed, BitmapFactory will only parse the original width and height information of the picture, and will not actually load the picture

  • inPreferredConfig: Used to configure the picture decoding method, corresponding to the type Bitmap.Config. If it is not null, it will be used to decode the picture. The default value is Bitmap.Config.ARGB_8888

  • inBitmap: The inBitmap setting was introduced in Android 3.0. By setting this parameter, the Bitmap that has been created before can be used when the picture is loaded to save memory and avoid creating a Bitmap again. In Android 4.4, it is added that the size of the picture set by inBitmap is different from the size of the picture that needs to be loaded, as long as the picture of inBitmap is larger than the current picture that needs to be loaded.

  • inDensity
    represents the pixel density of this bitmap, and this value is related to the drawable directory where this picture is placed.
    inDensity assignment:
    drawable-ldpi 120
    drawable-mdpi 160
    drawable-hdpi 240
    drawable-xhdpi 320
    drawable-xxhdpi 480

  • inTargetDensity
    represents the pixel density of the target (screen) when it is to be drawn,
    the way to get it in the code: getResources().getDisplayMetrics().densityDpi

  • inScreenDensity

    /**
     * Decode a new Bitmap from an InputStream. This InputStream was obtained from
     * resources, which we pass to be able to scale the bitmap accordingly.
     * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
     *         is {@link android.graphics.Bitmap.Config#HARDWARE}
     *         and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
     *         is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
     *         function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
     */
    @Nullable
    public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
    
    
        validate(opts);
        if (opts == null) {
    
    
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
    
    
        	//value就是读取资源文件时,资源文件的一些数据信息
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
    
    
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
    
    
            	//inDensity赋值的是资源所在文件夹对应的密度
                opts.inDensity = density;
            }
        }
        
        if (opts.inTargetDensity == 0 && res != null) {
    
    
        	//inTargetDensity赋值的是手机屏幕的像素密度densityDpi
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }

Through these parameters of BitmapFactory.Options, we can load the reduced image at a certain sampling rate, and then use the reduced image in ImageView. This will reduce memory usage, avoid OOM, and improve the performance of Bitamp loading.

This is actually what we often call image size compression. Size compression is the calculation method of compressing the pixels of a picture and the memory size of a picture: the color type of the picture's pixel * width * height , by changing the three values ​​to reduce the memory occupied by the picture to prevent OOM, of course this way It may distort the picture.

Bitmap.Config

public static enum Config {
    
    
    ALPHA_8,//每个像素占用1byte内存
    RGB_565,//每个像素占用2byte内存
    ARGB_4444,//每个像素占用2byte内存
    ARGB_8888;//每个像素占用4byte内存;默认模式
}

Bitmap memory usage calculation

Bitmap, as a bitmap, needs to read in the data of the image at each pixel, which mainly occupies the memory, that is, these pixel data. The total size of the pixel data of a picture is the pixel size of the picture * the byte size of each pixel. Usually you can understand this value as the size of the memory occupied by the Bitmap object. The pixel size of the picture is horizontal pixel value * vertical pixel value. So there is the following formula:

Bitmap memory ≈ total size of pixel data = horizontal pixel value * vertical pixel value * memory for each pixel

Without holding a Bitmap object, calculate how much memory the picture in the drawable resource directory occupies after being loaded as a Bitmap

When BitmapFactory.decodeResource loads a picture into the memory, the width and height of the generated Bitmap are not equal to the width and height of the original picture, but will be scaled according to the density of the current screen.

The size of Bitmap in memory depends on:

  • Color format, if it is ARGB8888, it is 4 bytes per pixel, if it is RGB565, it is 2 bytes
  • Resource directory where original picture files are stored
  • The density of the target screen (so under the same conditions, the memory consumed by Redmi in terms of resources must be less than that of Samsung S6)
int realWidth = (int) (rawWidth * targetDensity / (float) rawDensity + 0.5f)
int realHeight = (int) (rawHeight * targetDensity / (float) rawDensity + 0.5f) 
int memory = realWidth * realHeight * bytes_for_current_colorMode;

rawWidth is the original width of the resource image
targetDensity is the density of the current screen (inTargetDensity variable in the Android source code)
rawDensity is the density corresponding to the resource folder where the resource image is located (inDensity variable in the Android source code)
bytes_for_current_colorMode is the current color format The number of bytes corresponding to each pixel

For example:
ARGB_8888 color format:
ARGB occupies 8 bits each, and each pixel occupies memory (8bit + 8bit + 8bit + 8bit)/8=4Byte, so the memory occupied by Bitmap is realWidth * realHeight * 4
RGB_565 color format:
R5 bit, G6 Bit, B5 bit, each pixel occupies memory (5bit + 6bit + 5bit)/8=2Byte, so the memory occupied by Bitmap is realWidth * realHeight * 2

Corresponds to the variables in the Android source code:

int realWidth = (int) (rawWidth * inTargetDensity / (float) inDensity + 0.5f)
int realHeight = (int) (rawHeight * inTargetDensity / (float) inDensity + 0.5f) 
int memory = realWidth * realHeight * bytes_for_current_colorMode;

Our usual understanding is to directly multiply the width of the picture by the height, and then multiply it by the memory size occupied by a single pixel in the current Bitmap format. This algorithm ignores two points:
1. When the Android device loads Bitmap, it will scale the pictures stored in such directories as drawable-hdpi, drawable-xhdpi, drawable-xxhdpi..., so here you need to take the original width of the picture. High to perform scaling calculations.

2. If the first point is taken into consideration, the calculated memory size of bitmap is slightly different from bitmap.getByteCount(). This difference is because the result of "(rawWidth * inTargetDensity / (float) inDensity + 0.5f)" is a float type, and the number of pixels in the picture must be an integer. So there is a rounding process, and the error comes from here.

After understanding the above principles, we can draw the following conclusions:
1. On the same device, the smaller the density (dpi) corresponding to the resource directory where the image file is stored, the larger the width and height of the loaded Bitmap, and the memory occupied Also bigger. Similarly, the larger the dpi of the resource directory where the image is located, the smaller the size of the generated bitmap.
2. The higher the pixel density of the device screen, the larger the size of the generated bitmap
3. The density value corresponding to the res/drawable directory is the same as the res/drawable-mdpi directory, equal to 1, and the dpi value is 160.
4. If the pixel density of the resource directory where the image file is stored is the same as the pixel density of the device screen, the generated bitmap will not be scaled and the size is the original size.
Therefore, the previous calculation formula of bitmap memory can be evolved into:
bitmap memory ≈ total size of pixel data = picture pixel width * picture pixel height * (pixel density of the device screen/pixel density of the resource directory where the picture file is stored)^2 * Memory per pixel = Pixel width of the picture * Pixel height of the picture * Memory per pixel

Bitmap memory optimization:
From the above formula, it is not difficult to see that there are three main ways to optimize Bitmap memory:

  • When loading Bitmap, select a low-color quality parameter (Bitmap.Config), such as RGB_5665, so that compared to the default ARGB_8888, the memory usage is reduced by half. It is suitable for scenes that require relatively low color diversity.
  • Place the pictures in a reasonable resource directory and keep them as consistent as possible with the screen density. But don't put all of them in the resource directory with the highest density. The pixel density of the resource directory is higher than the screen density, and the size of the loaded Bitmap will be smaller than the original size, or even smaller than the size of the display area, which cannot meet some requirements.
  • According to the size of the target control, when loading the picture, the size of the bitmap is scaled. For example, on a screen with a pixel density of 480dpi and an ImageView with a width of 300dp and a height of 200dp, the unscaled image resolution that can be displayed is 900*600. If the image resolution is larger than this size, it is necessary to consider scaling down when parsing. .

Existing Bitmap object, calculate the memory size occupied by Bitmap

Just call the getByteCount() method of Bitmap.

getByteCount(): Returns the minimum number of bytes that can be used to store this bitmap pixel. Its internal calculation method: byte size of each row * total number of rows (i.e. height)

    /**
     * Returns the minimum number of bytes that can be used to store this bitmap's pixels.
     *
     * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can
     * no longer be used to determine memory usage of a bitmap. See {@link
     * #getAllocationByteCount()}.</p>
     */
    public final int getByteCount() {
    
    
        if (mRecycled) {
    
    
            Log.w(TAG, "Called getByteCount() on a recycle()'d bitmap! "
                    + "This is undefined behavior!");
            return 0;
        }
        // int result permits bitmaps up to 46,340 x 46,340
        return getRowBytes() * getHeight();
    }

Bitmap memory compression

Do pictures of the same width and height in different formats have the same memory usage?

When we use pictures, choose jpg, png or webp, will it affect the memory?
As long as the original width and height of the image are the same and they are placed in the same folder, the memory occupied by Bitmap after loading is the same. For example: R.drawable.icon_mv_jpg, R.drawable.icon_mv_png, R.drawable.icon_mv_webp three pictures have the same width and height, and they are placed in the same folder, so
BitmapFactory.decodeResource loads three pictures on the same mobile screen The width and height are the same, and the default color quality parameter of BitmapFactory.Options configuration is ARGB_8888,
so the memory occupied by these three pictures is the same when loaded into the memory, although the disk occupied by these three pictures The size is different.

Picture compression realization

The parameters of BitmapFactory.options mainly used: When
inJustDecodeBounds
is true, the decoder will return null, but the outxxx field will be parsed

inPreferredConfig
sets the pixel format of the picture after decoding, such as ARGB_8888/RGB_565

inSampleSize
sets the image decoding zoom ratio, if the value is 2, the width and height of the loaded image is 1/2 of the original, and
the memory size of the entire image is 1/4 of the original image

/**
 * 图片压缩
 */
public class ImageResize {
    
    


    /**
     * 返回压缩图片
     *
     * @param context
     * @param id
     * @param maxW
     * @param maxH
     * @param hasAlpha
     * @return
     */
    public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha, Bitmap reusable) {
    
    

        Resources resources = context.getResources();

        BitmapFactory.Options options = new BitmapFactory.Options();
        // 设置为true后,再去解析,就只解析 out 参数
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeResource(resources, id, options);

        int w = options.outWidth;
        int h = options.outHeight;


        options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);

        if (!hasAlpha) {
    
    
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }

        options.inJustDecodeBounds = false;

        // 复用, inMutable 为true 表示易变
        options.inMutable = true;
        options.inBitmap = reusable;


        return BitmapFactory.decodeResource(resources, id, options);

    }

    /**
     * 计算 缩放系数
     *
     * @param w
     * @param h
     * @param maxW
     * @param maxH
     * @return
     */
    private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {
    
    

        int inSampleSize = 1;

        if (w > maxW && h > maxH) {
    
    
            inSampleSize = 2;

            while (w / inSampleSize > maxW && h / inSampleSize > maxH) {
    
    
                inSampleSize *= 2;
            }

        }

        return inSampleSize;
    }
}

Bitmap memory optimization-memory reuse

The inBitmap memory reuse mechanism that comes with Bitmap mainly refers to the reuse of memory blocks. There is no need to reapply for a new memory for this bitmap, which avoids a memory allocation and recycling, thereby avoiding OOM and memory jitter, and improves operating efficiency.

InBitmap is similar to the technical principle of the object pool, avoiding the performance loss caused by the frequent creation and destruction of memory. Using inBitmap can improve the cycle efficiency of bitmap.

In the second quarter performance optimization released by Google, inBitmap technology is mentioned https://www.youtube.com/watch?v=_ioFW3cyRV0&index=17&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE (requires over the wall)

Before using inBitmap, each bitmap created requires an exclusive block of memory. After
Insert picture description here
using inBitmap, multiple bitmaps will reuse the same block of memory.
Insert picture description here
Therefore, using inBitmap can greatly improve the efficiency of memory utilization, but it also has several restrictions:
1. inBitmap can only be used in Use after SDK 11 (Android 3.0). On Android 2.3, the bitmap data is stored in the native memory area, not on the Dalvik memory heap.
2. Between SDK 11 -> 18, the size of the reused bitmap must be the same. For example, the size of the image assigned to inBitmap is 100-100, then the newly applied bitmap must also be 100-100 before it can be reused.
Starting from SDK 19, the newly applied bitmap size must be less than or equal to the bitmap size that has been assigned.
3. The newly applied bitmap and the old bitmap must have the same decoding format. For example, everyone is ARGB_8888. If the previous bitmap is ARGB_8888, then the ARGB_4444 and RGB_565 format bitmaps cannot be supported, but you can create a bitmap that contains more A typical reusable bitmap object pool, so that subsequent bitmap creation can find a suitable "template" for reuse.

The best way here is to use LRUCache to cache the bitmap. When you cache a new bitmap later, you can find the most suitable bitmap for reuse from the cache according to the api version to reuse its memory area.

google official Bitmap related tutorial:
http://developer.android.com/training/displaying-bitmaps/manage-memory.html
http://developer.android.com/training/displaying-bitmaps/index.html

Bitmaps that require memory reuse cannot call recycle() to reclaim memory

As shown below:

Insert picture description here

The entryRemoved method is called back when LruCache removes the image. In this method, we should deal with it separately:

  1. When it can be reused (oldValue.isMutable() is to judge whether it can be reused), we reuse it through the reuse pool
  2. If it cannot be reused, we will directly call recycle() to recycle

The above picture is processed like this, that is, no matter what the situation, recycle() will be called to recycle our bitmap and release the occupied memory. This will cause us to later take out the Bitmap from the reuse pool for reuse and find that the memory of the Bitmap has been It was recycled, so an error was reported.

So the above should be changed to the following:

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
    
    
				if (oldValue.isMutable()) {
    
    
				    // < 3.0  bitmap 内存在 native
				    // >= 3.0 在 java
				    // >= 8.0 又变为 native
				    // 如果从内存缓存中移除,将其放入复用池
				    reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
				} else {
    
    
				    oldValue.recycle();
				}
			}

Bitmap memory cache

Use LruCache cache tool class: android.util.LruCache

lru (least recently used), that is, the least recently used, the core idea is: recently used data is more likely to be used in the future.

Implementation method:
Use linked list + HashMap (ie LinkedHashMap). When a new data item needs to be inserted, if the new data item exists in the linked list (generally called a hit), the node is moved to the head of the linked list, if it does not exist, a new node is created and placed at the head of the linked list. If the cache is full, delete the last node of the linked list. When accessing data, if the data item exists in the linked list, move the node to the head of the linked list, otherwise return -1. In this way, the node at the end of the linked list is the data item that has not been accessed the most recently.

Insert picture description here

Bitmap disk cache

Use DiskLruCache
https://github.com/JakeWharton/DiskLruCache

Bitmap long image loading

使用:android.graphics.BitmapRegionDecoder

How to use:
InputStream is = getAssets().open("big.png");

// The long image is loaded using BigmapRegionDecoder
// true: the input stream is shared, and the input stream will not be used if the input stream is closed, so this place uses false
BigmapRegionDecoder decoder = BigmapRegionDecoder.newInstance(is,false);

// Get the picture of the specified Rect area, which is equivalent to taking a screenshot from the big picture
// options is set to null
Bitmap bitmap = decoder.decodeRegion(Rect,null);

For example: How to load the long picture on the right into the phone.
Insert picture description here
If you put it directly, it will make the contents unclear:
Insert picture description here

The correct solution:
zoom the long image to the same width as the screen, and then scale the length proportionally to display an area of ​​the long image, and then continuously move the area of ​​the long image when sliding.
Insert picture description here

Reference:
[Android memory optimization] Bitmap memory usage calculation (Bitmap image memory usage analysis | Bitmap memory usage calculation | Bitmap conversion between different pixel densities)
Android performance optimization: Bitmap detailed explanation & how much memory does your Bitmap occupy?
Image loading and memory optimization of
Bitmap Accurate calculation formula for the memory size occupied by Bitmap

Guess you like

Origin blog.csdn.net/yzpbright/article/details/109209028