Bitmaps are an inevitable part of development, and they cause a lot of headaches, and while we can now use open source libraries to largely avoid OOM, it’s worth taking the time to figure them out.

Let’s analyze Bitmap from the following aspects. First of all, what factors determine the memory occupied by loading an image? Then how do we rationalize the adjustment of these factors affecting memory.


How is memory occupied

Bitmap.getbytecount () can be used at run time to get the memory used to load this Bitmap. So this way we can see how loading images consumes memory, let’s look at the source code.

public final int getByteCount() {
    return getRowBytes() * getHeight();
}Copy the code

As you can see, memory consumption is determined by both getRowBytes and image height. What is getRowBytes?

public final int getRowBytes() {
    return nativeRowBytes(mNativePtr);
}Copy the code

NativeRowBytes () is the low-level function.

size_t SkBitmap::ComputeRowBytes(Config c, int width) {
    return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}

static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
    return width * SkColorTypeBytesPerPixel(ct);
}
static int SkColorTypeBytesPerPixel(SkColorType ct) {
   static const uint8_t gSize[] = {
    0.// Unknown
    1.// Alpha_8
    2.// RGB_565
    2.// ARGB_4444
    4.// RGBA_8888
    4.// BGRA_8888
    1.// kIndex_8
  };
  SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1),
                size_mismatch_with_SkColorType_enum);

   SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
   return gSize[ct];
}Copy the code

We found getRowBytes layers of query () is determined by the width and SkBitmapConfigToColorType width, this is the Type of Bitmap display format, we often use ARGGB_8888 suggests that a pixel take up 4 bytes. So we come to the conclusion that the Bitmap loads like this: display format * width * height.

So we can start from the two aspects of display format and picture size.


Efficient loading of Bitmaps

In the above analysis, we know that display format and image size are important factors affecting Bitmap loading. Android provides a BitmapFactory class to handle loading images onto bitmaps. It provides static methods to do this, depending on the source from which the images are loaded.

// Parse the length byte as a bitmap from the data in a byte array starting at offset
decodeByteArray(byte[] data, int offset, int lenght);

// Parse the file pathName into bitmap objects
decodeFile(String pathName);

// A Bitmap object is parsed from the given stream, and the options specified by Options apply when loading the object into memory
decodeStream(InputStream is, Rect outPadding, Options opts)

// A Bitmap object is parsed from the given resource according to its ID, and the options specified by Options apply when loading the object into memory
decodeResource(Resource res, int id, Options opts);

// Parse a Bitmap object from the given stream
decodeStream(InputStream is);Copy the code

If we look at the static method above, we can see an Options class. What is it? What is it for? In fact, it is a static inner class of BitmapFactory, and we can see it by looking at its properties. We can think of it as a bitmap property setting, such as setting the bitmap’s zoom factor, display format, etc. Through the Options class, we can do a lot of things, including preventing OOM.

public static class Options { 

  public Options() { 
    inDither = false; 
    inScaled = true; 
    inPremultiplied = true; 
  } 

  public Bitmap inBitmap;          // It is used to implement the reuse of Bitmap, as described below
  public int inSampleSize;         / / sampling rate
  public boolean inPremultiplied; 
  public boolean inDither;         // Whether to enable jitter
  public int inDensity;            // The original resource density in the previous article
  public int inTargetDensity;      // Target screen density
  public boolean inScaled;         // Whether scaling is supported
  public int outWidth;             // The original width of the image
  public int outHeight;            // The original height of the image. }Copy the code

The Options internal class controls the memory footprint of bitmap loading. How do we do this? By looking at the internal classes and our earlier analysis of how bitmaps consume memory, we found that these are the areas to start with.

1. Bitmap display format

Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;

The default bitmap display format is ARGB_8888, and we know from our analysis above that ARGB_8888 takes up 4 bytes, so if we change the display format to RGB_565 takes up 2 bytes, the bitmap memory consumption should be reduced by half.

Size of 2.

In the above analysis, we know that in addition to the display format affect the bitmap memory, more decisive factor is the width of the picture is high, we imagine, we request a high network clearly the figure, the resolution is high, and most likely in our app just shows a thumbnail, if counterparts to load all the high clearly the figure, If it is not OOM, inSampleSize is displayed in Options.

3. The sampling rate

When inSampleSize= 1, load the original image. When inSampleSize=2, the width and height of the image are 1/2, so the memory usage is 1/4. We generally agree that inSampleSize should be an integer multiple of 2.

For example, an 800*800 image is loaded into imageView, imageView is 200*200, so the zoom factor is 4, and we set the display format of options to RGB_565, so how much memory is used before and after the comparison? Before: 4*800*800 = 2.4M; After: 2*200*200 = 78k; The effect can be seen.

If the imageView is 200 by 200, but the image is 800 by 400 is the sampling rate 4 or 2? If it is 4, the image is 200*100, and if it is 2, the image is 400*200. You can see that when it is 4, the image 200*100 will be stretched into the imageView of 200*200, so when the width and height sampling rate are different, the smaller one will be used.

4. Scaling factor

We can see the inScaled property by looking at the Options property. We interpreted it as the scaling coefficient. We should know that the UI usually marks our graph with PX, so we need to transform it to write XML. This is because of density. What is density? Read this article and you will immediately know what density is. Let’s take an example of how scaling affects memory.

Huawei XX mobile phone (inTargetDensity=320) loaded xxHDPI file picture (480). Image resolution 850*1203, image address.

private void test(){
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.girl);
        Log.i("Display density of Huawei phones :", getResources().getDisplayMetrics(a).densityDpi+"");
        Log.i("Image loading memory size:", bitmap.getByteCount() +"b");
        Log.i("Image loading memory size:", bitmap.getByteCount(a) /1024+"k");
    }Copy the code

Memory consumption: 850*320/480*1203*320/480*4= 1.73m.

Let’s manually set the display density:

private void test(){
        BitmapFactory.Options opts = new BitmapFactory.Options(a);
        opts.inDensity = 160; // 480 was changed to 160
        opts.inTargetDensity = 320; / / the same
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.girl, opts);
        Log.i("Display density of Huawei phones :", getResources().getDisplayMetrics(a).densityDpi+"");
        Log.i("Image loading memory size:", bitmap.getByteCount() +"b");
        Log.i("Image loading memory size:", bitmap.getByteCount(a) /1024+"k");
    }Copy the code

So, 850*320/160*1203*320/160*4= 15.6m, so the memory consumption is increased, the scaling factor is 3 times the original memory consumption is 9 times the original.

In general, we don’t deal with scaling coefficients.

5. Here are the dry goods

Having said that, let’s give a common solution.

Resources res;  // Resource files
int resId;      // Resource file id
int imageW;     / / imageView wide
int imageH;     / / imageView is high

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

options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = getSampleSize(options);
options.inJustDecodeBounds = false;

options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory,decodeResource(res, resId, options);

// Calculate the sampling rate
public int getSampleSize(BitmapFactory.Options options){
    final int width = options.outWidth;    // The original width of the image
    final int height = options.outHeight;  // The original height of the image
    int inSampleSize = 1;
    if( width>imageW || height>imageH ){
        final int halfW = width/2;
        final int halfH = height/2;
        while(
            ((halfW/inSampleSize) >= imageW) &&
            ((halfH/inSampleSize) >= imageH )){
            inSampleSize *= 2; }}return inSampleSize;
}
Copy the code

The code above will only point you may have doubts, we loaded the two bitmap, respectively is the options. InJustDecodeBounds set the true and false after each loading time, loading is lost twice?

No, the name of the variable suggests that when inJustDecodeBounds is set to True, we are not actually loading the image, only resolving some raw information (such as width and height), so this operation is extremely lightweight and we get the width and height to calculate the sampling rate.

After calculating the sampling rate, set inJustDecodeBounds to false and then actually load the image, using our series of slimmdown bitmaps to achieve efficient loading.

In fact, there are many more ways to make loading bitmaps efficient, such as caching, which will be covered in the next article.