origin


Image compression is almost always a problem when we write Android applications. In addition to the app icon, there are basically only two sources for the image we want to display:

  • From the Internet
  • Load in local album

Whether downloaded from the Internet or read from the system’s image library, the image has a common feature: high pixels. At the same time, as we all know, the Memory allocated to each application by The Android system is limited, and the memory required to parse and load an image is much larger than the image itself. Therefore, at this time, the program may take up too much memory, thus appearing OOM phenomenon. So what is the OOM?

Exception java.lang.OutOfMemoryError: Failed to allocate a 916 byte allocation with 8388608 free bytes and 369MB until OOM; failed due to fragmentation (required continguous free 65536 bytes for a new buffer where largest contiguous free 32768 bytes)
java.nio.CharBuffer.allocate (CharBuffer.java:54)
java.nio.charset.CharsetDecoder.allocateMore (CharsetDecoder.java:226)
java.nio.charset.CharsetDecoder.decode (CharsetDecoder.java:188)
org.java_websocket.util.Charsetfunctions.stringUtf8 (Charsetfunctions.java:77)
org.java_websocket.WebSocketImpl.decodeFrames (WebSocketImpl.java:375)
org.java_websocket.WebSocketImpl.decode (WebSocketImpl.java:158)
org.java_websocket.client.WebSocketClient.run (WebSocketClient.java:185)
java.lang.Thread.run (Thread.java:818)
Copy the code

OOM is OutOfMemory exception, also known as memory overflow, which usually shows that the application flashes back. So how do we start to solve it?

The solution


First of all, we found that we have to load the images, the resolution of the mobile phone screen is much higher than us, what’s more, we are on a thumb size control, to load a 4 k larger view is completely unnecessary, that is to say, if we can make each control to display the size of the corresponding image, then the problem would be solved

So, how to achieve the picture and control of the seat? At this point we introduced the image compression scheme:

  • First, get the original image size
  • Second, get the control size
  • Next, get our image to control ratio
  • Finally, based on this ratio, the image is compressed to a size suitable for display

So let’s get started:

Gets the original size


As we all know, Android provides a BitmapFactory class that has decodeResource() decodeFile() decodeStream() and so on:

public static Bitmap decodeResource(Resources res, int id)

public static Bitmap decodeFile(String pathName)

public static Bitmap decodeStream(InputStream is)
Copy the code

Among them:

  • DecodeResource () : Used to parse resource files, images in the RES folder
  • DecodeFile () : used to parse the pictures in the system album
  • DecodeStream () : decodeStream() is used to parse images in the input and output streams, usually downloaded from HttpClient

The other methods are not mentioned here, as we can see in the source code that almost all of them end up parsing the image into a stream and then calling the decodeStream() method to instantiate our Bitmap object.

Although these methods are familiar to most of us, there is an important inner class that is often overlooked by some beginners: bitmapFactory.options, which is essential for image compression. The Options object is used to determine the parameters of the Bitmap (the target image) to be generated. It’s easy to use. Let’s start with a new bitmapFactory.options object. Then call the method with Options, as in

  • public static Bitmap decodeResource(Resources res, int id, Options opts)
  • public static Bitmap decodeResourceStream(@Nullable Resources res,@Nullable TypedValue value,@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts)

After the call, we find that in addition to returning an instantiated Bitmap, the length, width, type and other properties of the Options object are also set to the corresponding properties of our image. So, it’s easy to think: pass in the Options object to get the original size of the image, in preparation for later compression. We pass in the Options object together with the ID of a 4K image in Resources to try to get its size. The program OOM crashed!

Why does this happen? First of all, why did we get the Options object? In order to obtain the size of the picture; So why do we need to get the original size? Is it to compress the image size to display size in proportion to the size of the control? So why should we compress it to the right size? Because if you call the corresponding decode in its original size… () method parsing pictures, will cause the memory usage is too high trigger OOM exception, and then cause the program crash ah! Unexpectedly, we ended up calling the corresponding decode in order to get Options… () method, yes Options is copied, but since this method is used to generate images, that is, Bitmap objects. So the app crashed while trying to parse this huge image

So is there no way out?

Yes, AS I mentioned earlier: Option has many internal parameters, one of which is called inJustDecodeBounds. This parameter defaults to false. But if we set this parameter to true first, the method does not generate the corresponding Bitmap, but only measures the various attributes of the image, such as length, width, type, etc., and then returns null. So it’s easy to imagine setting inJustDecodeBounds to true and then calling the corresponding decode… Finally, change the value of inJustDecodeBounds back to false. This approach has two advantages:

  1. Both image sizes can be obtained due to subsequent operations
  2. And successfully avoid having to parse the image, resulting in a programOOMAnd collapse.

But this is the point that many people miss.

Ok, now give the concrete implementation:

    public static void calculateOptionsById(@NonNull Resources res,@NonNull BitmapFactory.Options options, int imgId) {
        BitmapFactory.decodeResource(res, imgId, options);
    }
Copy the code

You may notice that it is only setting inJustDecodeBounds to True but not back to False, because getting Options is just the first step in image compression, which we will change later

How to compress


Let’s continue to look at the composition of Options. We found out that there was a data member named inSampleSize, and he was the key, so what does that mean?

Let me give you an example. Let’s say I have a 4000 by 1000 pixel image here:

  • When we putinSampleSizeThe value of the set4The resulting image size will be 1000 x 250 pixels
  • When we putinSampleSizeThe value of the set5The resulting image size will be 800 x 200 pixels. What’s the concept?

It’s not just a quarter of the length and a fifth of the width, it’s the size of the picture, it’s one over n^2! In other words:

  • If the original image2MB, then wheninSampleSizeThe assignment for4It just needs to be loaded0.125 MB
  • What ifinSampleSizeThe assignment for5? Only need to0.08 MB! even100kEven less small picture ah!

So I will give the concrete implementation of this method:

    public static int calculateInSamplesizeByOptions(@NonNull BitmapFactory.Options options, int reqWidth, int reqHeight) {
        int inSamplesize   = 1;
        int originalWidth  = options.outWidth;
        int originalHeight = options.outHeight;
        if (originalHeight > reqHeight || originalWidth > reqWidth) {
            int heightRatio = originalHeight / reqHeight;
            int widthRatio  = originalWidth  / reqWidth;
            inSamplesize = heightRatio > widthRatio ? heightRatio : widthRatio;
        }
        return inSamplesize;
    }
Copy the code

And what we find is that I’ve calculated, first of all, the ratio of the size of the original image to the size of the target, in the ternary operator, we assign inSamplesize to be the larger one. Why not use the smaller one? I’m going to keep it a secret here, but feel free to post your thoughts in the comments section

Generate target image


After the previous two steps, you can already outline the last step, the idea is very simple:

  1. Mister into aOptionsobject
  2. willThe Options of inJustDecodeBoundsSet totrue
  3. Then I call method onecalculateOptionsByIdGet the original size toOptionsIn the
  4. Call method threecalculateInSamplesizeByOptionsGet the correspondinginSampleSizeobject
  5. willOptionstheinJustDecodeBoundsChanged backfalse
  6. Call againdecode... (a)Method (here isdecodeResource) to get the compressedBitmapobject

The concrete implementation is as follows

    public static Bitmap decodeBitmapById (@NonNull Resources res, int resId, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        calculateOptionsById(res, options, resId);
        options.inSampleSize = calculateInSamplesizeByOptions(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeResource(res, resId, options);
        return bitmap;
    }
Copy the code

Very good, let’s check it out:

Great, almost the same as the original picture, but the smoothness of the software is greatly improved! But is this really perfect?

The most perfectionist of us might have an idea: if the person calling our method, or us at a particular time. Don’t want to use the already written decodeBitmapById method, but as you through the first two methods: calculateOptionsById calculateInSamplesizeByOptions to implement image compression, the problem is this:

  • callcalculateOptionsByIdBefore you might forget, setinJustDecodeBoundtrue, which leads to the calculation of large graphs, directly occurOOM
  • Call outcalculateInSamplesizeByOptionsAfter may forget, setinJustDecodeBoundsfalse, which leads to unavailabilityBitmapObject, look confused
  • I’m done with everythingcalculateInSamplesizeByOptionsI didn’t assign the unreturned value tooptions.inSampleSizeAll this trouble was for nothing

Therefore, we need to optimize:

First of all, in calculateOptionsById will default options. InJustDecodeBounds is set to true:

    public static void calculateOptionsById(@NonNull Resources res,@NonNull BitmapFactory.Options options, int imgId) {
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, imgId, options);
    }
Copy the code

Second, in the final, calculateInSamplesizeByOptions will default options. InJustDecodeBounds set to false:

    public static int calculateInSamplesizeByOptions(@NonNull BitmapFactory.Options options, int reqWidth, int reqHeight) {
        int inSamplesize   = 1;
        int originalWidth  = options.outWidth;
        int originalHeight = options.outHeight;
        if (originalHeight > reqHeight || originalWidth > reqWidth) {
            int heightRatio = originalHeight / reqHeight;
            int widthRatio  = originalWidth  / reqWidth;
            inSamplesize = heightRatio > widthRatio ? heightRatio : widthRatio;
        }
        options.inJustDecodeBounds = false;
        return inSamplesize;
    }
Copy the code

Why not assign options.inSampleSize after this method? This is just in case, sometimes we just want to get the scale and do something else, and we don’t want to change the property, so it’s up to the user to decide whether to assign it or not

conclusion


Ok, so far, all the potholes related to image compression have been summarized, so let’s start with a side view:

  1. With the help ofoptions.inJustDecodeBoundsParameters of the assignmenttrueWhen not generating the image feature, the original image size is saved inOptions
  2. throughoptionsThe ratio of the original map size to the target size, yesoptions.inSampleSizeset
  3. Generate target image
  4. The compression problem was solved, but it was too much trouble to compress the image every time you opened it! I’ll try to solve this problem more effectively in the following sections, but you can keep an eye on the programming world of _Yuanhao

Related articles


Android gives your Room a free ride on the RxJava bandwagon from repetitive code

The founder of the ViewModel and ViewModelProvider. Factory: the ViewModel

Singleton pattern – globally available Context objects, enough for this article

Scale gesture ScaleGestureDetector source code analysis, this article is enough

ObjectAnimator, ValueAnimator, ObjectAnimator, ValueAnimator

After watching this animation frame of Never View again, I kneeled and rubbed the clothes board

Android custom clock control hour, minute hand, second hand drawing this article is enough

Android custom control – draw clock dial

Android Advanced custom ViewGroup Custom layout

Android Drawable animation, this article is enough

Welcome to attention_yuanhaoThe nuggets!


Regularly share Android development wet goods, the pursuit of humor and depth of the article perfect unity.

Source Demo link: Drop my first time to write the Android project, I hope you song star~ thank you!

Thumb up, please! Because your encouragement is the biggest power that I write!