Android picture performance optimization: Bitmap

1, Introduce

As a large memory consumer, pictures have always been the key object that developers try to optimize. Bitmap's memory is located in native before 3.0, then changed to jvm, and then changed back to native after 8.0. Each jvm process has a memory limit, while native has no limit (not without impact, at least not oom). Therefore, moving bitmap, a large memory user, to native may be a dream of many people, but the management and implementation of native is obviously more complex than that of jvm. Unless there is a ready-made implementation, few people move this piece.

2, Getting to know Bitmap

Bitmap is a final class, so it cannot be inherited. Bitmap has only one construction method, and the construction method is not modified by any access modifier, that is, the construction method is friendly, but Google says that the construction method of bitmap is private (private), which feels a little lax. Anyway, in general, we can't directly create a bitmap object through the construction method.

Bitmap is one of the most important classes in image processing in Android system. Bitmap can obtain image file information, cut, rotate, scale and compress the image, and save the image file in the specified format.

3, Create a Bitmap object

Since you can't create a Bitmap directly through the construction method, how can you create a Bitmap object. Generally, we can use the static method createBitmap() of Bitmap and the static method decode series of BitmapFactory to create Bitmap objects.

  • Static method of Bitmap createBitmap()
  • BitmapFactory's decode series static methods

4, Color configuration information and compression mode information of Bitmap

There are two internal enumeration classes in Bitmap: config and CompressFormat. Config is used to set color configuration information and CompressFormat is used to set compression mode.

Config resolution:

  • Bitmap.Config.ALPHA_8: Color information only consists of transparency, accounting for 8 bits.
  • Bitmap.Config.ARGB_4444: color information consists of transparency and R (Red), G (Green) and B (Blue). Each part occupies 4 bits, accounting for 16 bits in total.
  • Bitmap.Config.ARGB_8888: color information consists of transparency and R (Red), G (Green) and B (Blue). Each part occupies 8 bits, accounting for 32 bits in total. It is the default color configuration information of bitmap, and it also takes up the most space.
  • Bitmap.Config.RGB_565: color information consists of R (Red), G (Green) and B (Blue). R accounts for 5 bits, G for 6 bits and B for 5 bits, accounting for 16 bits in total.

Generally, when we optimize Bitmap, we usually use Bitmap when we need to do performance optimization or prevent OOM (Out Of Memory) Config. RGB_ 565 this configuration because Bitmap Config. ALPHA_ 8 only transparency, displaying general pictures is meaningless, Bitmap Config. ARGB_ 4444 the picture displayed is not clear, Bitmap Config. ARGB_ 8888 takes up the most memory.

CompressFormat resolution:

  • Bitmap.CompressFormat.JPEG: refers to image compression with jpeg compression algorithm. The compressed format can be ". jpg" or ". jpeg", which is a lossy compression.
  • Bitmap.CompressFormat.PNG: refers to image compression with png compression algorithm. The compressed format can be ". png", which is a lossless compression.
  • Bitmap.CompressFormat.WEBP: refers to image compression with webp compression algorithm. The compressed format can be ". webp", which is a lossy compression. Under the condition of the same quality, the volume of webp format image is 40% smaller than that of JPEG format image. The drawback is that the encoding time of webp format image is "8 times longer than JPEG format image".

5, Bitmap picture performance optimization

Method 1: use inSampleSize sampling rate compression, size compression:

BitmapFactory is through BitmapFactory Options operates on the image, and then generates a Bitmap object from the operated image, or saves the operated image with an existing Bitmap. When it cannot be saved, null will be returned.
BitmapFactory. Common fields in options are:

Use options Injustdecodebounds to get the original size, and then use options as needed Insamplesize to sample the image close to the view size.

BitmapFactory can bring an Options when decoding pictures. It has some useful functions, such as:

  • inTargetDensity: pixel density drawn on the target Bitmap.
  • inSampleSize: compress the image. This value is an int. when it is less than 1, it will be treated as 1. If it is greater than 1, the width and height of the bitmap will be reduced and the resolution will be reduced according to the proportion (1 / inSampleSize). When it is greater than 1, this value will be treated as an integer power of 2 or an integer power close to 2. For example, if width=100, height=100, inSampleSize = 2 (set to 2), the bitmap will be processed as width=50, height=50, width and height will be reduced to 1 / 2, and the number of pixels will be reduced to 1 / 4
  • inJustDecodeBounds: if set to true, it means that the Bitmap object information is obtained, but its pixels are not loaded into memory. Sometimes you can use this just to get the size of the picture without directly loading the whole picture.
  • inPreferredConfig: the color configuration information of the Bitmap object. By default, the Bitmap.config is used Config. ARGB_ 8888. In this mode, a pixel will occupy 4 bytes (8 + 8 + 8 + 8 + 8 = 32 bits (4 bytes)). For some pictures that do not require transparency or low picture quality, RGB can be used_ 565, a pixel will only occupy 2 bytes (5 + 6 + 5 = 16 bits (2 bytes)), which can save 50% of memory.
  • Inpargeable and inputshareable need to be used together, bitmapfactory There are comments in the Java source code, which roughly means whether the bitmap can be recycled when the system memory is insufficient. It is a bit like a soft reference, but these two attributes have been ignored since 5.0, because the system thinks that decoding after recycling may actually lead to performance problems
  • inBitmap: an officially recommended parameter, which means to reuse picture memory and reduce memory allocation. Before 4.4, only picture memory areas of the same size can be reused. After 4.4, as long as the original picture is larger than the picture to be decoded, it can be reused.
  • inDensity: the density set for the Bitmap object. If inScaled is true (this is the default), and if the inDensity does not match the inTargetDensity, it will be scaled to match the inTargetDensity before the Bitmap object returns.
  • inDither: whether to dither the image. The default value is false.
  • inScaled: sets whether to scale.
  • outHeight: the height of the Bitmap object.
  • outWidth: the width of the Bitmap object.

Method 2: reasonably select the pixel format of Bitmap

The picture in ARGB8888 format occupies 4 bytes (8 + 8 + 8 + 8 + 8 + 8 = 32 bits (4 bytes)) per pixel, while RGB565 is 2 bytes (5 + 6 + 5 = 16 bits (2 bytes)). If it is ARGB8888, it is 4 bytes per pixel. If it is RGB565, it is 2 bytes.

formatdescribe
ALPHA_8There is only one alpha channel, and each pixel is stored in one byte (8 bits)
ARGB_4444This is not recommended from API 13 because the quality is too poor
ARGB_8888ARGB has four channels, and each pixel is stored in four bytes (32 bits)
RGB_565Each pixel occupies 2 bytes, of which red occupies 5bit, green occupies 6bit and blue occupies 5bit

ALPHA8 doesn't need to be used, because we can do it with any color.

Although ARGB4444 occupies only half of the memory of ARGB8888, it has been officially rejected.

ARGB8888, RGB565: ARGB will be used by default_ 8888. In this mode, a pixel will occupy 4 bytes, and RGB can be used for some pictures without transparency requirements or low picture quality requirements_ 565, a pixel will only occupy 2 bytes, which can save 50% of memory.

Method 3: put the pictures in the appropriate folder:

If the same picture is placed in different directories, it will generate bitmap s of different sizes. Because its width and height are scaled, the picture resources should be placed in high-density resource folders as much as possible, so as to save the memory cost of the picture. It is generally recommended to put it under xxhdpi. At present, the dpi is used in mainstream mobile phones, And when UI provides us with cut-off images, it should also be provided for high-density screen devices as far as possible

jpg is a lossy compressed image storage format, while png is a lossless compressed image storage format. Obviously, jpg will be smaller than png

The memory occupied by Bitmap actually depends on:

  • As we mentioned earlier, if it is ARGB8888, it means 4 bytes for a pixel (8 + 8 + 8 + 8 + 8 + 8 = 32 bits (4 bytes)); if it is RGB565, it means 2 bytes (5 + 6 + 5 = 16 bits (2 bytes));

  • The resource directory where the original files are stored (hdpi or xhdpi or xxhdpi can't be foolishly distinguished);

  • The density of the target screen (so under the same conditions, the memory consumed by Hongmi in terms of resources must be less than that of Samsung S6);

Mode 4: design a reasonable cache mechanism (memory cache, reuse pool, hard disk cache)

Memory cache: we can use the official LurCache that has been implemented for us;

Reuse pool: used to load the pictures discarded in the memory cache LRU but may be used immediately. We cache them in the reuse pool to prevent them from being recycled by the GC immediately;

Hard disk cache: implemented by Jake Wharton DiskLruCache , download address: https://github.com/JakeWharton/DiskLruCache

Why set a reuse pool?

Because in the official 8.0Android system, pictures are processed in the native layer, such as the recycling mechanism. We cannot intervene in the java layer. In order not to immediately hand over the pictures to the native layer for processing, we use the weak references in the reuse pool to temporarily save them in the java layer, so that we need to use the pictures just abandoned by the memory cache LRU, Instead of repeating the decode pictures and putting them into the LRU queue, the performance consumption is reduced.

6, Bitmap picture performance optimization code

Picture optimization requires three things:

  1. Pixel compression (format conversion)
  2. Memory reuse
  3. L3 cache design (memory, disk, network)

Pixel compression

// Used to optimize picture (pixel compression (format conversion))
public class ImageResize {
    // int maxW,int maxH -- defines the width and height of pixels
    public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha) {
        Resources resources = context.getResources();
        BitmapFactory.Options options = new BitmapFactory.Options();
//        Bitmap bitmap = BitmapFactory.decodeResource(resources,R.drawable.logo);
        // Only the relevant parameter information (width, height, etc.) is decoded, and the picture will not be generated
        options.inJustDecodeBounds = true; // Decoding action switch (on)
        BitmapFactory.decodeResource(resources, id, options);
        int w = options.outWidth;  // The width of the true output of the picture
        int h = options.outHeight; // The true output of the picture is high
        // Set the zoom factor (see picture)
        options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);

        if (!hasAlpha) {
            // Transparency is not required and RGB is used_ 565 (5 + 6 + 5 = 16 bits)
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }
        options.inJustDecodeBounds = false; // Close after processing
        // Return to the real picture after setting
        return BitmapFactory.decodeResource(resources, id, options);
    }

    // Calculate scaling factor
    // w. H: width and height of the original drawing
    // maxW,maxH: width and height after scaling
    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;
    }
}

Pixel compression + memory reuse

public class ImageResize {
    // int maxW,int maxH -- defines the width and height of pixels
    // reusable: memory reuse location
    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();
//        Bitmap bitmap = BitmapFactory.decodeResource(resources,R.drawable.logo);
        // Only the relevant parameter information (width, height, etc.) is decoded, and the picture will not be generated
        options.inJustDecodeBounds = true; // Decoding action switch (on)
        BitmapFactory.decodeResource(resources, id, options);
        int w = options.outWidth;  // The width of the true output of the picture
        int h = options.outHeight; // The true output of the picture is high
        // Set the zoom factor (see picture)
        options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);

        if (!hasAlpha) {
            // Transparency is not required and RGB is used_ 565 (5 + 6 + 5 = 16 bits)
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }
        options.inJustDecodeBounds = false; // Close after processing

        // Set to be reusable
        options.inMutable=true;
        // Set the memory location required for reuse
        options.inBitmap = reusable;

        // Return to the real picture after setting
        return BitmapFactory.decodeResource(resources, id, options);
    }

    // Calculate scaling factor
    // w. H: width and height of the original drawing
    // maxW,maxH: width and height after scaling
    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;
    }
}

The bitmap object applies for a piece of memory in memory. When bitmap2 needs to use it, it directly reuses the memory applied by bitmap (or directly overwrites the contents of bitmap)

Three level cache mechanism: memory cache, reuse pool and disk cache (memory, disk and network)

public class ImageCache {

    public static final String TAG = "ImageCache";

    private static ImageCache instance;
    private Context context;

    public static ImageCache getInstance() {
        if (null == instance) {
            synchronized (ImageCache.class) {
                if (null == instance) {
                    instance = new ImageCache();
                }
            }
        }
        return instance;
    }

    // Memory cache
    private LruCache<String, Bitmap> memoryCache;
    // Disk cache
    private DiskLruCache diskLruCache;

    BitmapFactory.Options options = new BitmapFactory.Options();

    // Reuse pool
    public static Set<WeakReference<Bitmap>> reuseablePool;

    // The parameter dir is the path of the last stored disk cache
    public void init(Context context, String dir) {

        this.context = context.getApplicationContext();
        // synchronizedSet: a set with locks
        reuseablePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());

        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        int memoryClass = am.getMemoryClass();// Available memory provided by virtual machine (memory available by APP)
        memoryCache = new LruCache<String, Bitmap>(memoryClass / 8 * 1024 * 1024) {// 1 / 8 of the available memory is used for image caching (1 / 8 of the available memory of APP)

            /*
             *  return    value Occupied memory size
             */

            @Override
            // The sizeOf method is used to calculate the picture size
            protected int sizeOf(String key, Bitmap value) {
                // For compatibility with Android before 3.0 (before and after 19)
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
                    // Greater than 19 (can be compressed and multiplexed)
                    return value.getAllocationByteCount();
                }
                // 19 before reusing memory, only pictures of the same size can be used to reuse memory
                return value.getByteCount();
            }

            @Override
            // After LruCache is full, some data will be extruded (extruded in oldValue)
            // newValue: the object put by the queue head
            // oldValue: the object extruded at the end of the queue
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                // oldValue needs to be put into the reuse pool
                if (oldValue.isMutable()) {
                    // Use WeakReference to associate with reference queue
                    reuseablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
                } else {
                    oldValue.recycle();
                }
            }
        };

        try {
            // Open a directory on your phone
            // Parameter 3: 1 file
            diskLruCache = DiskLruCache.open(new File(dir), 1, 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Reference queue
    ReferenceQueue referenceQueue;
    Thread clearReferenceQueue;
    boolean shutDown;

    // API for actively monitoring GC to speed up recycling (compatible with different Android versions)
    private ReferenceQueue<Bitmap> getReferenceQueue() {
        if (null == referenceQueue){
            referenceQueue = new ReferenceQueue<Bitmap>();
            // Open a single thread to scan the content scanned by GC in the reference queue and hand it over to the native layer for release
            clearReferenceQueue = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!shutDown){
                        try {
                            // remove with blocking function
                            Reference<Bitmap> reference = referenceQueue.remove();
                            Bitmap bitmap = reference.get();
                            if (null!=bitmap && !bitmap.isRecycled()){
                                bitmap.recycle(); // Improve and speed up the recycling action (go to the native layer for recycling)
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            clearReferenceQueue.start();
        }
        return referenceQueue;
    }

    /**
     * Add memory cache
     */
    public void putBitmapToMemeory(String key,Bitmap bitmap){
        memoryCache.put(key,bitmap);
    }

    public Bitmap getBitmapFromMemory(String key){
        return memoryCache.get(key);
    }

    public void clearMemoryCache(){
        memoryCache.evictAll();
    }

    // Get content in reuse pool
    public Bitmap getReuseable(int w,int h,int inSampleSize){
        // Ignore below 3.0
        if (Build.VERSION.SDK_INT<Build.VERSION_CODES.HONEYCOMB){
            return null;
        }
        Bitmap reuseable = null;
        // Through iterator
        Iterator<WeakReference<Bitmap>> iterator = reuseablePool.iterator();
        while (iterator.hasNext()){
            Bitmap bitmap = iterator.next().get();
            if (null!=bitmap){
                // Reusable
                if (checkInBitmap(bitmap,w,h,inSampleSize)){
                    reuseable = bitmap;
                    iterator.remove();
                    Log.i(TAG, "Found in reuse pool");
                    break;
                }else {
                    iterator.remove();
                }
            }
        }
        return reuseable;
    }

    // Detect whether it can be reused
    private boolean checkInBitmap(Bitmap bitmap, int w, int h, int inSampleSize) {
        if (Build.VERSION.SDK_INT<Build.VERSION_CODES.KITKAT){
            return bitmap.getWidth()==w && bitmap.getHeight()==h && inSampleSize == 1;
        }
        // If the scaling factor is greater than 1, it can be reused
        if (inSampleSize>=1){
            w/=inSampleSize;
            h/=inSampleSize;
        }
        int byteCount = w*h*getPixelsCount(bitmap.getConfig());
        return byteCount<=bitmap.getAllocationByteCount();
    }

    /*
     *  The number of bytes required to obtain different formats of pixels
     */
    private int getPixelsCount(Bitmap.Config config) {
        if (config == Bitmap.Config.ARGB_8888){
            return 4;
        }
        return 2;
    }

    // Processing of disk cache
    /*
     *  Add disk cache
     */
    public void putBitMapToDisk(String key,Bitmap bitmap){
        DiskLruCache.Snapshot snapshot = null;
        OutputStream os = null;
        try {
            snapshot = diskLruCache.get(key);
            // If this file already exists in the cache, ignore it
            if (null==snapshot){
                // If there is no such file, it will be generated
                DiskLruCache.Editor editor = diskLruCache.edit(key);
                if (null!=editor){
                    os = editor.newOutputStream(0);
                    bitmap.compress(Bitmap.CompressFormat.JPEG,50,os);
                    editor.commit();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (null!=snapshot){
                snapshot.close();
            }
            if (null!=os){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /*
     *  Fetch from disk cache
     */
    public Bitmap getBitmapFromDisk(String key,Bitmap reuseable){
        DiskLruCache.Snapshot snapshot = null;
        Bitmap bitmap = null;
        try {
            snapshot = diskLruCache.get(key);
            if (null==snapshot){
                return null;
            }
            // Get the file input stream and read the bitmap
            InputStream is = snapshot.getInputStream(0);
            // Decode pictures and write
            options.inMutable = true;
            options.inBitmap = reuseable;
            bitmap = BitmapFactory.decodeStream(is,null,options);
            if (null != bitmap){
                memoryCache.put(key,bitmap);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (null!=snapshot){
                snapshot.close();
            }
        }
        return bitmap;
    }
}

Complete code

BitMapAppDemo

reference resources

Performance optimization topic 3 - memory optimization (picture L3 cache)
Play Android Bitmap

Keywords: Android Optimize

Added by munky334 on Sun, 16 Jan 2022 12:53:03 +0200