Android Bitmap prevents memory overflow

1.Bitmap

Bitmap is often used in Android development, and improper use of Bitmap can easily cause OOM.

The formula for calculating the memory size occupied by Bitmap is: image width × image height × number of bytes occupied by a pixel, so reducing any value of these three parameters can reduce the memory size occupied by bitmap (you can also use Bitmap .getAllocationByteCount() method to view the memory size occupied by Bitmap).

Therefore, optimization is required when using Bitmap to prevent memory overflow problems. There are two optimization methods: ① reduce bitmap memory usage; ② reuse bitmap space that already occupies memory or use existing bitmap, for example, use LruCache caching mechanism when there are a lot of pictures.

 

2. Reduce memory usage

①Reduce width and height BitmapFactory.Options.inSampleSize

inSampleSize is a property of BitmapFactory.Options, changing it can change the width and height of the picture. If this value is set to a value greater than 1 (a value less than 1 is 1), the decoder is requested to subsample the original image, returning a smaller image to save memory.

For example, if inSampleSize = 4, the width of the returned image is 1/4 of the original width, the height is 1/4 of the original height, and the number of pixels is 1/16 of the original number of pixels.

The inSampleSize property is usually used in conjunction with the inJustDecodeBounds property. If inJustDecodeBounds is set to true, the decoder will return null (no bitmap), but the outWidth/outHeight fields will still be set, allowing the caller to query the bitmap without allocating memory for its pixels .

private fun sampleCompress(requestWidth: Int, requestHeight: Int) {

    val options = BitmapFactory.Options()

    options.inJustDecodeBounds = true //Do not allocate memory space, only calculate image size

    BitmapFactory.decodeResource(resources, R.mipmap.timg, options)

    Log.d(TAG, "bitmap outWidth:${options.outWidth}") //Original picture width

    Log.d(TAG, "bitmap outHeight:${options.outHeight}") //Original image height

    // Calculate the zoom factor according to the width and height requirements

    var sampleSize = 1

    if (requestWidth < options.outWidth || requestHeight < options.outHeight) {

        sampleSize = max(options.outWidth * 1.0/requestWidth, options.outHeight * 1.0/requestHeight).toInt()

    }

    options.inJustDecodeBounds = false

    options.inSampleSize = sampleSize

    val bitmap = BitmapFactory.decodeResource( resources, R.mipmap.timg, options)

    logBmInfo(bitmap) //scaled picture

}

d1028b67a97940309e3c133866d5a0ed.png

The width and height of the original Bitmap are 1000. If the width and height are required to be 500, the inSampleSize is 2. According to the log, the memory size occupied by the original Bitmap is 4000000B, but the width and height are compressed using the inSampleSize attribute, thereby reducing the memory usage to 1/4 of the original.

②Reduce the number of bytes occupied by each pixel BitmapFactory.Options.inPreferredConfig

inPreferredConfig is a property of BitmapFactory.Options. The default value is Bitmap.Config.ARGB_8888. Changing this configuration can change the number of bytes occupied by a pixel .

In this attribute, A represents transparency, R represents red, G represents green, and B represents blue.

Bitmaps use small grids of pixels to describe images, and a computer screen is actually a grid containing a large number of pixels. In the bitmap, the image you usually see is determined by the position and color value of the pixel in each grid. The color of each point is fixed, and the type of color value of each pixel produces different Bitmap Config, the common ones are:

1) ALPHA_8: Indicates an 8-bit Alpha bitmap, A occupies 8 bits, has no color, only transparency, and each pixel occupies 1 byte of memory.

2) ARGB_4444 (obsolete): Indicates a 16-bit ARGB bitmap, that is, A occupies 4 bits, R occupies 4 bits, G occupies 4 bits, and B occupies 4 bits, occupying 2 bytes in total.

3) ARGB_8888: Indicates a 32-bit ARGB bitmap, that is, A occupies 8 bits, R occupies 8 bits, G occupies 8 bits, B occupies 8 bits, and each pixel occupies 4 bytes of memory.

4) RGB_565: Indicates a 16-bit RGB bitmap, that is, R occupies 5 bits, G occupies 6 bits, and B occupies 5 bits. There is no transparency, and each pixel occupies 2 bytes of memory.

private fun argbCompress() {

    val bm1 = BitmapFactory.decodeResource( resources, R.mipmap.timg)

    logBmInfo(bm1)

    val options = BitmapFactory.Options()

    // Set the configuration to RGB_565

    options.inPreferredConfig = Bitmap.Config.RGB_565

    val bm2 = BitmapFactory.decodeResource( resources, R.mipmap.timg, options)

    logBmInfo(bm2)

From the execution log results, we can see that the memory usage of the optimized Bitmap is half of the size of the unoptimized Bitmap, and the length and width have not changed . RGB_565 does not have a large visual image for images that do not require transparency.

e51a0d65ca2b41b0b2628c05a65c824d.png

 

3. Error-prone: Compression cannot change the size of memory occupied by bitamp

Bitmap's compress(Bitmap.CompressFormat format, int quality, OutputStream stream) method is to write the compressed version of the bitmap to the specified output stream. This method may take several seconds to complete, so it is best to call it in a child thread. (Note: Not all formats directly support all bitmap configurations, so bitmaps returned from BitmapFactory may have different bit depths and may lose per-pixel alpha values ​​(e.g. JPEG only supports opaque pixels)).

The parameters of the compress method: format is the compressed image format, quality is the compression quality (according to the format, the quality compression effect is also different), and stream is the output stream for writing compressed data.

The format is Bitmap.CompressFormat.JPEG, compressed according to quality 0-100;

If the format is Bitmap.CompressFormat.PNG, the quality parameter will be invalid, because the PNG image is lossless and cannot be compressed;

The format is Bitmap.CompressFormat.WEBP, which saves more space than JPEG, and is compressed according to quality 0-100.

Compression loses color accuracy and requires less storage space, but using the compressed stream to regenerate the Bitmap will not change the size of the memory occupied by the bitmap, because the width and height of the bitmap have not changed, and Bitmap.Config has not changed , that is, the number of bytes occupied by a pixel has not changed, so the memory occupied by the final bitmap has not changed.

private fun bitmapCompress(bitmap: Bitmap){

    val out = ByteArrayOutputStream()

    Log.d(TAG, "———— JPEG ————")

    bitmap.compress( Bitmap.CompressFormat.JPEG, 30, out)

    Log.d(TAG, "out byte count:${out.size()}")

    val jpegArray = out.toByteArray()

    val jpeg = BitmapFactory.decodeByteArray( jpegArray, 0, jpegArray.size)

    logBmInfo(jpeg)

 

    out.reset()

    Log.d(TAG, "———— PNG ————")

    bitmap.compress( Bitmap.CompressFormat.PNG, 30, out)

    Log.d(TAG, "out byte count:${out.size()}")

    val pngArray = out.toByteArray()

    val png = BitmapFactory.decodeByteArray( pngArray, 0, pngArray.size)

    logBmInfo(png)

    

    out.reset()

    Log.d(TAG, "———— WEBP ————")

    bitmap.compress( Bitmap.CompressFormat.WEBP, 30, out)

    Log.d(TAG, "out byte count:${out.size()}")

    val webpArray = out.toByteArray()

    val webp = BitmapFactory.decodeByteArray( webpArray, 0, webpArray.size)

    logBmInfo(webp)

}

The results from the execution log are as follows. When the quality is 30 for compression, the storage space occupied by the JPEG format and WEBP becomes smaller (not necessarily that the storage space occupied by the WEBP format is smaller than that of the JPEG format), while the PNG format is not compressed. The streams of the three are re-decoded into bitmap, and it can be seen that the memory size occupied by the bitmap has not changed.

af79ffe172e546329ee4b1a257c25303.png

The compressed stream is re-decoded to generate a bitmap, and it will be displayed that the PNG format has no effect, and the image quality of the JPEG format and WEBP format has deteriorated significantly.

 

4.Bitmap multiplexing

In addition to reducing the memory usage of the bitmap, there are also solutions to optimize, that is, reuse the bitmap space that has already occupied memory or use the existing bitmap.

①重用BitmapFactory.Options.inBitmap

inBitmap is a property of BitmapFactory.Options, which can be set to reuse the bitmap space that has occupied memory .

But Bitmap reuse has certain limitations:

1) Before Android 4.4, only the Bitmap memory area of ​​the same size can be reused;

2) Any Bitmap area can be reused after 4.4, as long as the memory is larger than the Bitmap to be allocated;

3) The reused bitmap is mutable.

Taking Android 4.4 and later as an example, first query the width and height of the bitmap to be loaded by setting options.inJustDecodeBounds to true, and then judge whether the reuseBitmap is eligible for reuse. If yes, assign it to the options.inBitmap property, and finally get the desired bitmap , that is, the memory space of reuseBitmap is reused.

private fun getBitmap(): Bitmap {

    val options = BitmapFactory.Options()

    options.inJustDecodeBounds = true

    BitmapFactory.decodeResource(resources, R.mipmap.timg, options)

    // Determine whether the reuse condition is satisfied, here assume that Bitmap.Config is ARGB_8888 to calculate the memory size

    if (reuseBitmap.allocationByteCount >= options.outWidth * options.outHeight * 4) {

        // reuseBitmap is a mutable reuse bitmap

        options.inBitmap = giantBitmap

    }

    options.inJustDecodeBounds = false

    return BitmapFactory.decodeResource( resources, R.mipmap.timg, options)

}

②LruCache

When using RecyclerView, if the itemView contains pictures, the bitmap will be continuously recreated when sliding, thus wasting memory space. At this point, LruCache can be used to cache the bitmap, and it can be taken out of the cache when needed again without recreating it.

private val memoryCache = object : LruCache<String, Bitmap>(4*1024*1024) { // Cache 4M images

    override fun sizeOf(key: String, value: Bitmap): Int {

        // Inform lruCache bitmap of memory size

        return value.allocationByteCount

    }

}

fun putBitmap(key: String, bitmap: Bitmap) {

    memoryCache.put(key, bitmap)

}

fun getBitmap(key: String): Bitmap? {

    return memoryCache.get(key)

}

 

5. Load the giant image

When loading images, generally in order to avoid OOM as much as possible, the following methods will be followed:

1) For image display: Compress and display the image according to the size of the display image control.

2) If the number of pictures is very large: a caching mechanism such as LruCache will be used to maintain the content occupied by all pictures within a range.

In fact, there is another situation for image loading, that is, a single image is very huge, and compression is not allowed. For example, display: world map, Qingming Shanghe map, Weibo long map, etc.

For this requirement, compression is not allowed first, and the screen must be loaded according to the size of the original image, so the screen must not be large enough, and considering the memory situation, it is impossible to load the entire image into the memory at one time, so it must be partially loaded. Then you need to use a class: BitmapRegionDecoder. Secondly, since the screen cannot be displayed completely, it is necessary to add a gesture of dragging up, down, left, and right so that the user can drag to view.

①BitmapRegionDecoder

BitmapRegionDecoder is mainly used to display a certain rectangular area of ​​an image.

BitmapRegionDecoder provides a series of newInstance methods to construct objects, and supports passing in file paths, file descriptors, and input streams of files, etc. for example:

BitmapRegionDecoder bitmapRegionDecoder  =BitmapRegionDecoder.newInstance(inputStream, false);

The image that needs to be processed is passed in here, and the next step is to specify the display area:

Bitmap Bitmap = bitmapRegionDecoder.decodeRegion(rect, options);

The first parameter is obviously a rect, and the second parameter is BitmapFactory.Options, through which you can control the inSampleSize, inPreferredConfig, etc. of the image. The return value is the loaded partial image.

Example of using BitmapRegionDecoder:

InputStream inputStream = getAssets().open( "world.jpg");

// Get the width and height of the image

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

tmpOptions.inJustDecodeBounds = true;

BitmapFactory.decodeStream(inputStream, null, tmpOptions);

int width = tmpOptions.outWidth;

int height = tmpOptions.outHeight;

//Set the central area of ​​the displayed image

BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance( inputStream, false);

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

options.inPreferredConfig = Bitmap.Config.RGB_565;

Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);

mImageView.setImageBitmap(bitmap);

In this way, BitmapRegionDecoder is used to load the picture in assets, and bitmapRegionDecoder.decodeRegion is called to parse the middle rectangular area of ​​the picture, and the bitmap is returned, which is finally displayed on the ImageView.

② Customize the display big picture control

In order to swipe to view the entire image, you can customize a control to display the giant image. First, the range of Rect is the size of the custom View, and then according to the user's movement gestures, continuously update the parameters of Rect.

Refer to Hongyang Dashen https://blog.csdn.net/lmj623565791/article/details/49300989/

Guess you like

Origin blog.csdn.net/zenmela2011/article/details/129923600