Talk about some knowledge points of Bitmap

Talk about some knowledge points of Bitmap

Official account : byte array , I hope to help you

Bitmap should be the type of resource that occupies the most memory space in many applications, and Bitmap is also one of the common causes of application OOM. For example, photos taken by the camera of a Pixel phone can reach up to 4048 * 3036 pixels (12 million pixels). If the bitmap used is configured as ARGB_8888 (the default setting for Android 2.3 and later), loading a single photo into the memory is approximately Requires 48MB of memory (4048 * 3036 * 4 bytes), such a huge memory demand may immediately exhaust all available memory of the application

This article will talk about some of the more useful knowledge points of Bitmap, I hope it will be helpful to you

The full text can be summarized into the following questions:

  1. The formula for calculating the memory size occupied by Bitmap?
  2. What is the relationship between the memory size of Bitmap and the drawable folder where it is located?
  3. What is the relationship between the memory size of Bitmap and the width and height of ImageView?
  4. How does Bitmap reduce the memory size?

1. Preliminary knowledge

Before starting to talk about the knowledge points about Bitmap, you need to explain some basic concepts as preliminary knowledge

We know that the px value corresponding to 1dp on different mobile phone screens may be very different. For example, 1dp may correspond to 1px on a small-screen mobile phone, and 3px on a large-screen mobile phone. This is also one of the principle foundations for our application to achieve screen adaptation. If you want to know how much px 1dp corresponds to on a particular mobile phone, or if you want to know the width and height of the screen, these information can be obtained through DisplayMetrics

Val DisplayMetrics = applicationContext.resources.displayMetrics duplicated code

Print out the DisplayMetrics information of the simulator used in this article:

Density = {DisplayMetrics 3.0 , width = 1080 , height = 1920 , scaledDensity = 3.0 , xdpi = 480.0 , ydpi = 480.0 } copy the code

Several pieces of information can be extracted from it:

  1. The density is equal to 3, indicating that 1dp is equal to 3px on the simulator
  2. The screen width and height are 1920 x 1080 px, which is 640 x 360 dp
  3. The screen pixel density is 480dpi

dpi is a very important value. It refers to the number of pixels per unit size specified on the system software. It is often a fixed value written in the system factory configuration file. The benchmark value of screen pixel density defined by the Android system is 160dpi. Under this benchmark value, 1dp is equal to 1px, and so on, under 320dpi, 1dp is equal to 2px.

dpi determines which folder the cut image is selected by the application when displaying the drawable. Each drawable folder corresponds to a different dpi size. The Android system will automatically select pictures from the appropriate drawable folder according to the actual dpi size of the current mobile phone. The dpi size corresponding to different suffixes is as shown in the following table. If the drawable folder name does not have a suffix, then the folder corresponds to 160dpi

drawableldpimdpihdpixhdpixxhdpixxxhdpi
dpi120dpi160dpi240dpi320dpi480dpi640dpi

For the 480dpi simulator used in this article, the application will give priority to

drawable-xxhdpi
Folder, if no picture is found in the folder, it will follow
xxxhdpi -> xhdpi -> hdpi -> mdpi -> ldpi
To search in the order of, the high-density version of image resources is preferred

2. Calculation formula of memory size

First save a picture with a size of 1920 x 1080 px to

drawable-xxhdpi
In the folder, and then display it on an ImageView with a width and height of 180dp, the memory occupied by the Bitmap is passed
bitmap.byteCount
To get

val options = BitmapFactory.Options() val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options) imageView.setImageBitmap(bitmap) log( "imageView width: " + imageView.width) log( "imageView height: " + imageView.height) log( "bitmap width: " + bitmap.width) log( "bitmap height: " + bitmap.height) log( "bitmap config: " + bitmap.config) log( "inDensity: " + options.inDensity) log( "inTargetDensity: " + options.inTargetDensity) log ( "Bitmap byteCount:" + bitmap.byteCount) copying the code
BitmapMainActivity: imageView width: 540 BitmapMainActivity: imageView height: 540 BitmapMainActivity: bitmap width: 1920 BitmapMainActivity: bitmap height: 1080 BitmapMainActivity: bitmap config: ARGB_8888 BitmapMainActivity: inDensity: 480 BitmapMainActivity: inTargetDensity: 480 BitmapMainActivity: Bitmap byteCount: 8294400 Copy the code
  • Since the density of the simulator is equal to 3, the width and height of the ImageView are both 540 px
  • The width and height of the Bitmap still maintain its original size, which is 1920 x 1080 px
  • ARGB_8888 represents the encoding format of the Bitmap, the next pixel of this format needs to occupy 4 bytes
  • inDensity
    Represents the drawable folder type finally selected by the system, which is equal to 480, which means it is taken
    drawable-xxhdpi
    Pictures under the folder
  • inTargetDensity
    Represents the dpi of the current device
  • 8294400 is the memory size occupied by Bitmap, the unit is byte

From the final result, the calculation formula of the memory size occupied by Bitmap can be easily deduced: bitmapWidth * bitmapHeight * the number of bytes occupied by a unit pixel , namely 1920 * 1080 * 4 = 8294400

3. The relationship with the drawable folder

The reason why the calculation formula of the memory size occupied by Bitmap is easily deduced above is because all the conditions are deliberately set to the optimal situation by me, which makes the calculation process so simple. In fact, the size of the memory occupied by Bitmap has a lot to do with the drawable folder where it is located, although the calculation formula has not changed

In order to achieve the best display effect, most of the current applications will prepare multiple sets of cuts for the application and place them in different drawable folders.

BitmapFactory.decodeResource
When the method decodes the Bitmap, it will automatically determine whether the image needs to be zoomed or displayed according to the dpi and drawable folder type of the current device

Change the picture from

drawable-xxhdpi
migrated to
drawable-xhdpi
Folder, and then print the log information

BitmapMainActivity: imageView width: 540 BitmapMainActivity: imageView height: 540 BitmapMainActivity: bitmap width: 2880 BitmapMainActivity: bitmap height: 1620 BitmapMainActivity: bitmap config: ARGB_8888 BitmapMainActivity: inDensity: 320. BitmapMainActivity: inTargetDensity: 480 BitmapMainActivity: Bitmap byteCount: 18.6624 million copy the code

As you can see, the width and height of the Bitmap have changed.

inDensity
Equal to 320 also shows that the selection is
drawable-xhdpi
For the pictures in the folder, the memory occupied by Bitmap has more than doubled

The dpi of the emulator is 480, and I got a dpi of 320

drawable-xhdpi
The pictures in the folder, in the understanding of the system, the folders are all small icons, which are prepared for small-screen mobile phones. If you want to display them on large-screen mobile phones, you need to enlarge them. The magnification ratio is 480./320 = 1.5 times, so the width of the Bitmap will become 1920 * 1.5 = 2880 px, and the height will become 1080 * 1.5 = 1620 px, and the final memory space occupied is 2880 * 1620 * 4 = 18662400

Therefore, for the same mobile phone, the size of the final memory occupied by Bitmap in different drawable folders is very relevant. Although the calculation formula has not changed, the system will automatically scale, resulting in the final width and height of the Bitmap. Changes have taken place, which affects the amount of memory space it occupies. Similarly, for the same picture in the same drawable folder, different mobile phone screens may also occupy different memory space, because the dpi size of different mobile phones may be different, and the scaling ratio of BitmapFactory is also Different

4. The relationship with the width and height of ImageView

In the previous example, the width and height of the Bitmap is 2880 * 1620 px, and the width and height of the ImageView is 540 * 540 px. The Bitmap will definitely be incomplete. Readers can try to change the width and height of the ImageView to verify whether it will Affect the size of Bitmap

I won t post the code here, just to conclude, the answer is that it doesn t matter . The reason is very simple, after all, the above example is the first Bitmap loaded into memory and then to set the ImageView, natural ImageView will not affect the loading process Bitmap, the size of the Bitmap only by their type in the drawable folder and mobile phone The dpi size is affected by these two factors. But this conclusion needs to consider the test method. If you are using Glide to load images, Glide implements an on-demand loading mechanism, which will automatically scale the Bitmap according to the size of the ImageView to avoid memory waste. In this case The width and height of the ImageView will affect the memory size of the Bitmap

5. BitmapFactory

BitmapFactory provides many methods for loading Bitmap objects:

decodeFile, decodeResourceStream, decodeResource, decodeByteArray, decodeStream
Wait many, but only
decodeResourceStream
with
decodeResource
These two methods will automatically scale according to the dpi. If the image is loaded from the disk or the assert directory, it will not be automatically scaled. After all, these sources do not have dpi information, and the resolution of the Bitmap can only maintain its original size.

decodeResource
Method will also be called
decodeResourceStream
method,
decodeResourceStream
If the method is judged
inDensity
with
inTargetDensity
If the two attributes are not actively assigned, they will be assigned according to the actual situation

@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 ) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { //If density is not assigned (equal to 0), then use the reference value 160 dpi opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { //Assign a value here, density is equal to the dpi corresponding to the drawable opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null ) { //If inTargetDensity is not actively set, inTargetDensity is equal to the dpi of the device opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); } Copy code

6. Bitmap.Config

Bitmap.Config defines four common encoding formats, namely:

  • ALPHA_8. Each pixel needs a byte of memory, only the transparency of the bitmap is stored, and there is no color information
  • ARGB_4444. A (Alpha), R (Red), G (Green), and B (Blue) each occupies four digits of precision, a total of 16 digits of precision, which is equivalent to two bytes, which means that one pixel occupies two bytes Memory, will store the transparency and color information of the bitmap
  • ARGB_8888. ARGB occupies eight bits of precision each, equivalent to four bytes, and will store the transparency and color information of the bitmap
  • RGB_565. R occupies five digits of precision, G occupies six digits of precision, B occupies five digits of precision, a total of 16 digits of precision, equivalent to two bytes, only color information is stored, no transparency information

7. Optimize Bitmap

According to the calculation formula of the memory size occupied by Bitmap: bitmapWidth * bitmapHeight * the number of bytes occupied by a unit pixel , if you want to minimize the memory size occupied by Bitmap, you must reduce the image resolution and reduce the number of bytes required per pixel. Consider these two aspects

At the beginning, the width and height of the Bitmap loaded is 1920 * 1080, and the occupied memory space is 1920 * 1080 * 4 = 8294400, which is about 7.9 MB, which is the state before optimization

val options = BitmapFactory.Options() val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options) imageView.setImageBitmap(bitmap) log( "bitmap width: " + bitmap.width) log( "bitmap height: " + bitmap.height) log( "bitmap config: " + bitmap.config) log( "inDensity: " + options.inDensity) log( "inTargetDensity: " + options.inTargetDensity) log( "bitmap byteCount: " + bitmap.byteCount) BitmapMainActivity: bitmap width: 1920 BitmapMainActivity: bitmap height: 1080 BitmapMainActivity: bitmap config: ARGB_8888 BitmapMainActivity: inDensity: 480 BitmapMainActivity: inTargetDensity: 480 BitmapMainActivity: Bitmap byteCount: 8294400 Copy the code

1, inSampleSize

Since the width and height of the ImageView is only 540 * 540 px, it will actually cause a lot of memory waste if it is loaded according to the original image. At this time, we can compress the image size through the inSampleSize property

For example, after setting inSampleSize to 2, the width and height of the Bitmap will be reduced to half of the original size, and the occupied memory space will become a quarter of the original size, 960 * 540 * 4 = 2073600, which is about 1.9 MB

val options = BitmapFactory.Options() options.inSampleSize = 2 val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options) imageView.setImageBitmap(bitmap) log( "bitmap width: " + bitmap.width) log( "bitmap height: " + bitmap.height) log( "bitmap config: " + bitmap.config) log( "inDensity: " + options.inDensity) log( "inTargetDensity: " + options.inTargetDensity) log( "bitmap byteCount: " + bitmap.byteCount) BitmapMainActivity: bitmap width: 960 BitmapMainActivity: bitmap height: 540 BitmapMainActivity: bitmap config: ARGB_8888 BitmapMainActivity: inDensity: 480 BitmapMainActivity: inTargetDensity: 480 BitmapMainActivity: Bitmap byteCount: 2073600 Copy the code

Can be seen, inSampleSize how much property should be set according to need the actual width and height of a Bitmap and the actual width and height of the ImageView to decide together these two conditions. We need to get the actual width and height of the Bitmap before officially loading the Bitmap, which can be achieved through the inJustDecodeBounds property. After setting inJustDecodeBounds to true

decodeResource
The method only reads the width and height attributes of the Bitmap without actually loading it. This operation is relatively lightweight. Then through each cycle of half reduction, calculate how much inSampleSize needs to be set to be as close as possible to the actual width and height of the ImageView, and then set inJustDecodeBounds to false to actually load the Bitmap

It should be noted that the final value used in inSampleSize will be rounded down to the nearest power of 2, and BitmapFactory will automatically verify and correct this value.

val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options) val inSampleSize = calculateInSampleSize(options, imageView.width, imageView.height) options.inSampleSize = inSampleSize options.inJustDecodeBounds = false val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options) imageView.setImageBitmap(bitmap) fun calculateInSampleSize (options: BitmapFactory . Options , reqWidth: Int , reqHeight: Int ) : Int { //Raw height and width of image val (height: Int , width: Int ) = options.run {outHeight to outWidth} var inSampleSize = 1 if (height> reqHeight || width> reqWidth) { val halfHeight: Int = height/ 2 val halfWidth: Int = width/ 2 //Calculate the largest inSampleSize value that is a power of 2 and keeps both //height and width larger than the requested height and width. while (halfHeight/inSampleSize >= reqHeight && halfWidth/inSampleSize >= reqWidth) { inSampleSize *= 2 } } return inSampleSize } Copy code

2. inTargetDensity

If we don t actively set inTargetDensity,

decodeResource
The method will automatically scale the Bitmap according to the dpi of the current device. We can control the scaling by actively setting inTargetDensity to control the final width and height of the Bitmap. The final width and height generation rules: 180/480 * 1920 = 720, 180/480 * 1080 = 405, the occupied memory space is 720 * 405 * 4 = 1166400, about 1.1 MB

val options = BitmapFactory.Options() options.inTargetDensity = 180 val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options) imageView.setImageBitmap(bitmap) log( "bitmap width: " + bitmap.width) log( "bitmap height: " + bitmap.height) log( "bitmap config: " + bitmap.config) log( "inDensity: " + options.inDensity) log( "inTargetDensity: " + options.inTargetDensity) log( "bitmap byteCount: " + bitmap.byteCount) BitmapMainActivity: bitmap width: 720 BitmapMainActivity: bitmap height: 405 BitmapMainActivity: bitmap config: ARGB_8888 BitmapMainActivity: inDensity: 480 BitmapMainActivity: inTargetDensity: 480 BitmapMainActivity: Bitmap byteCount: 1.1664 million copy the code

3. Bitmap.Config

The encoding picture format used by BitmapFactory by default is ARGB_8888, and each pixel occupies four bytes. We can change the picture format to be used as needed. For example, if the Bitmap to be loaded does not contain a transparent channel, we can use RGB_565. This format occupies two bytes per pixel, and the memory space occupied is 1920 * 1080 * 2 = 4147200, which is about 3.9 MB

val options = BitmapFactory.Options() options.inPreferredConfig = Bitmap.Config.RGB_565 val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_awe, options) imageView.setImageBitmap(bitmap) log( "bitmap width: " + bitmap.width) log( "bitmap height: " + bitmap.height) log( "bitmap config: " + bitmap.config) log( "inDensity: " + options.inDensity) log( "inTargetDensity: " + options.inTargetDensity) log( "bitmap byteCount: " + bitmap.byteCount) BitmapMainActivity: bitmap width: 1920 BitmapMainActivity: bitmap height: 1080 BitmapMainActivity: bitmap config: RGB_565 BitmapMainActivity: inDensity: 480 BitmapMainActivity: inTargetDensity: 480 BitmapMainActivity: Bitmap byteCount: 4.1472 million copy the code