I am just a small Android dish, write technical documents just to summarize their knowledge learned in the recent, never dare to be a teacher, if there are some incorrect places please point out, thank you!

1. An overview of the

In the design of Android applications, it is almost inevitable to load and display images. Due to the size of different images, some may require only tens of KB of memory space, while others may require tens of MB of memory space. Or one image doesn’t take up much memory, but loads and displays multiple images at the same time.

In these cases, loading images takes up a lot of memory, and the Android system has a limited amount of memory allocated to each process. If the amount of memory required for loading images exceeds the limit, the process will appear OOM, or run out of memory.

This paper adopts different loading methods for two different scenarios, such as loading large images or loading multiple images at one time, to avoid possible memory overflow problems.

2. Load large images

Sometimes loading and displaying an image takes up a lot of memory. For example, if the image size is 2592×1936 and the bitmap configuration is ARGB_8888, the memory size is 2592x1936x4 bytes, which is about 19MB. Simply loading such an image might exceed the process’s memory limit and cause an overflow of memory, so it certainly can’t be loaded directly into memory for actual use.

In order to avoid memory overflow, different loading methods are adopted according to different display requirements:

  • Display the entire content of an image: perform the original imageCompression shows.
  • Display part of an image: apply to the original imageLocal display.

2.1 Image compression display

Image compression display refers to the compression of the length and width of the original image, so as to reduce the memory occupation of the image, so that it can be displayed normally in the application, and ensure that there is no memory overflow in the loading and display process. BitmapFactory is a tool class for creating Bitmap objects, which can be used to generate Bitamp objects from different sources. During the creation process, you can also configure and control the objects to be generated. The BitmapFactory class declaration is as follows:

Creates Bitmap objects from various sources, including files, streams,and byte-arrays.
Copy the code

Because before loading the images, is unable to predict in advance the size of the image, so must according to the size of the image in front of the actual load and memory of the current process to determine whether to need to compress images, if the loading pictures of the memory space is more than the process is intended to provide or can provide memory size, must consider compressed images.

2.1.1 Determine the length and width of the original picture

In simple terms, the compression picture is to reduce the length and width of the original picture in accordance with a certain proportion, so first to determine the length and width information of the original picture. In order to obtain the image width information, using BitmapFactory decodeResource (Resources res, int id, the Options opts) interface, the statement is as follows:

    /**
     * Synonym for opening the given resource and calling
     * {@link #decodeResourceStream}.
     *
     * @param res   The resources object containing the image data
     * @param id The resource id of the image data
     * @param opts null-ok; Options that control downsampling and whether the
     *             image should be completely decoded, or just is size returned.
     * @return The decoded bitmap, or null if the image data could not be
     *         decoded, or, if opts is non-null, if opts requested only the
     *         size be returned (in opts.outWidth and opts.outHeight)
     * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
     *         is {@link android.graphics.Bitmap.Config#HARDWARE}
     *         and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
     *         is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
     *         function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
     */
    public static Bitmap decodeResource(Resources res, int id, Options opts) {
Copy the code

From this function declaration, you can see that the interface can get the length and width information of the image, and because null is returned, no memory is allocated, avoiding unnecessary memory allocation.

To get information about the width and length of the image, you must pass an Options parameter, with the inJustDecodeBounds set to true, stating:

   /** * If set to true, the decoder will return null (no bitmap), but * the out...  fields will still be set, allowing the caller to * query the bitmap without having to allocate the memory for its pixels. */
    public boolean inJustDecodeBounds;
Copy the code

Here is an example code to get information about the length and width of the image:

    BitmapFactory.Options options = new BitmapFactory.Options();
    // Specifies that only edge information is parsed and no bitmap objects are created when image files are parsed.
    options.inJustDecodeBounds = true;
    // r.davable. test is a 2560x1920 test image resource file.
    BitmapFactory.decodeResource(getResources(), R.drawable.test, options);
    int width = options.outWidth;
    int height = options.outHeight;
    Log.i(TAG, "width: " + width + ", height: " + height);
Copy the code

In the actual test, the length and width information obtained is as follows:

    01-05 04:06:23.022 29836 29836 I Android_Test: width: 2560, height: 1920
Copy the code

2.1.2 Determine the target compression ratio

After knowing the length and width of the original image, the target compression ratio must be determined in order to be able to perform subsequent compression operations. The so-called compression ratio refers to the cutting ratio of the original length and width. If the original picture is 2560×1920, the compression ratio is 4, the compressed picture is 640×480, and the final size is 1/16 of the original picture. The compression ratio in bitmapFactory. Options corresponds to inSampleSize, which is declared as follows:

    /** * If set to a value > 1, requests the decoder to subsample the original * image, returning a smaller image to save memory. The sample size is * the number of pixels in either dimension that correspond to a single * pixel in the decoded bitmap. For example, inSampleSize == 4 returns * an image that is 1/4 the width/height of the original, and 1/16 the * number of pixels. Any value <= 1 is treated the same as 1. Note: the * decoder uses a final value based on powers of 2, any other value will * be rounded down to the nearest power of 2. */
    public int inSampleSize;
Copy the code

Note that inSampleSize can only be a power of 2. If the value passed in does not meet the criteria, the decoder will select a power of 2 that is the least expensive than the value passed in. If the value passed in is less than 1, the decoder uses 1 directly.

To determine the final compression ratio, first determine the target size, that is, the length and width of the compressed target image information, according to the original length and width and target length and width to select a most appropriate compression ratio. Sample code is shown below:

    / * * *@param originWidth the width of the origin bitmap
     * @param originHeight the height of the origin bitmap
     * @param desWidth the max width of the desired bitmap
     * @param desHeight the max height of the desired bitmap
     * @return the optimal sample size to make sure the size of bitmap is not more than the desired.
     */
    public static int calculateSampleSize(int originWidth, int originHeight, int desWidth, int desHeight) {
        int sampleSize = 1;
        int width = originWidth;
        int height = originHeight;
        while((width / sampleSize) > desWidth && (height / sampleSize) > desHeight) {
            sampleSize *= 2;
        }
        return sampleSize;
    }
Copy the code

Note that desWidth and desHeight are the maximum length and width of the target image, not the final size, because the compression ratio determined by this method will ensure that the final image is no larger than the target. In the actual test, set the original image size to 2560×1920 and the target image size to 100×100:

    int sampleSize = BitmapCompressor.calculateSampleSize(2560.1920.100.100);
    Log.i(TAG, "sampleSize: " + sampleSize);
Copy the code

The test results are as follows:

    01-05 04:42:07.752  8835  8835 I Android_Test: sampleSize: 32
Copy the code

The resulting compression ratio is 32. If you use this ratio to compress a 2560×1920 image, you end up with an 80×60 image.

2.1.3 Compress images

In the first two parts, the length and width information of the original picture and the target compression ratio are determined respectively. In fact, the length and width of the original picture are determined to obtain the compression ratio. Now that the compression comparison has been obtained, the actual compression operation can be carried out. Only need to get inSampleSize through the Options passed to BitmapFactory. DecodeResource (Resources res, int id, the Options opts). Here is the sample code:

    public static Bitmap compressBitmapResource(Resources res, int resId, int inSampleSize) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
        options.inSampleSize = inSampleSize;
        return BitmapFactory.decodeResource(res, resId, options);
    }
Copy the code

2.2 Partial display of pictures

Image compression can be in a certain extent, affects the image quality and display effect, is not desirable in some scenarios, such as the map shows the request must be high quality images, can’t compress processing at this moment, in this scenario is not actually required to all part of a display image, consider a load, and only a certain part of the image, That is, *** shows *** locally.

To achieve the effect of partial display, you can use BitmapRegionDecoder to achieve, it is used to display a specific part of the picture, especially in the original picture is particularly large and not all loaded into the memory of the scene, its declaration is as follows:

    /** * BitmapRegionDecoder can be used to decode a rectangle region from an image. * BitmapRegionDecoder is particularly useful when an original image is large and * you only need parts of the image. * * 

To create a BitmapRegionDecoder, call newInstance(...) . * Given a BitmapRegionDecoder, users can call decodeRegion() repeatedly * to get a decoded Bitmap of the specified region. * */

public final class BitmapRegionDecoder {... }Copy the code

BitmapRegionDecoder is used to create a local display: newInstance() is used to create an instance, and decodeRegion() is used to create a Bitmap object on the image memory of the specified region, which is displayed in the display control.

Through BitmapRegionDecoder. NewInstance () to create a parser instance, its function declaration is as follows:

    /**
     * Create a BitmapRegionDecoder from an input stream.
     * The stream's position will be where ever it was after the encoded data
     * was read.
     * Currently only the JPEG and PNG formats are supported.
     *
     * @param is The input stream that holds the raw data to be decoded into a
     *           BitmapRegionDecoder.
     * @param isShareable If this is true, then the BitmapRegionDecoder may keep a
     *                    shallow reference to the input. If this is false,
     *                    then the BitmapRegionDecoder will explicitly make a copy of the
     *                    input data, and keep that. Even if sharing is allowed,
     *                    the implementation may still decide to make a deep
     *                    copy of the input data. If an image is progressively encoded,
     *                    allowing sharing may degrade the decoding speed.
     * @return BitmapRegionDecoder, or null if the image data could not be decoded.
     * @throws IOException if the image format is not supported or can not be decoded.
     *
     * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#KITKAT},
     * if {@link InputStream#markSupported is.markSupported()} returns true,
     * <code>is.mark(1024)</code> would be called. As of
     * {@link android.os.Build.VERSION_CODES#KITKAT}, this is no longer the case.</p>
     */
    public static BitmapRegionDecoder newInstance(InputStream is,
            boolean isShareable) throws IOException {... }Copy the code

Note that this is only one of the BitmapRegionDecoder newInstance function, in addition to other forms of implementation, readers interested in their own reference. After the BitmapRegionDecoder instance is created, the decodeRegion method can be called to create a local Bitmap object with the following function declaration:

    /**
     * Decodes a rectangle region in the image specified by rect.
     *
     * @param rect The rectangle that specified the region to be decode.
     * @param options null-ok; Options that control downsampling.
     *             inPurgeable is not supported.
     * @return The decoded bitmap, or null if the image data could not be
     *         decoded.
     * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
     *         is {@link android.graphics.Bitmap.Config#HARDWARE}
     *         and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
     *         is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
     *         function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
     */
    public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) {... }Copy the code

Since this part is relatively simple, here is the sample code:

    // Get the length and width of the original image, which is convenient for later local display to specify the area to be displayed.
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(getResources(), R.drawable.test, options);
    int width = options.outWidth;
    int height = options.outHeight;

    try {
        // Create a local parser
        InputStream inputStream = getResources().openRawResource(R.drawable.test);
        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputStream,false);
        
        // Specify the rectangle to display, the upper left quarter of the original image.
        Rect rect = new Rect(0.0, width / 2, height / 2);

        // Create bitmap configuration, here using RGB_565, each pixel is 2 bytes.
        BitmapFactory.Options regionOptions = new BitmapFactory.Options();
        regionOptions.inPreferredConfig = Bitmap.Config.RGB_565;
        
        // Create a Bitmap for the specified region and display it.
        Bitmap regionBitmap = decoder.decodeRegion(rect,regionOptions);
        ImageView imageView = (ImageView) findViewById(R.id.main_image);
        imageView.setImageBitmap(regionBitmap);
    } catch (Exception e) {
        e.printStackTrace();
    }
Copy the code

From the test results, it is true that only the upper left quarter of the original image is displayed, and the results are no longer posted here.

3. Load multiple images

Sometimes you need to display multiple images in your application at the same time. For example, if you use ListView,GridView, and ViewPager, you might want to display one image in each entry. This can get a little more complicated because you can swipe to change the control’s visibility. At the same time, the image of the invisible item continues to be in memory, and as it continues to increase, memory will overflow.

To avoid memory overflow, image resources corresponding to invisible items need to be reclaimed. That is, images related to the item are reclaimed when the item is slid out of the display area of the screen. In this case, the reclamation policy has a significant impact on the performance of the entire application.

  • Recycle immediately: Recycle images as soon as the current item slides off the screen, but if the item slides off the screen soon after, the image needs to be reloaded, which can lead to performance degradation.
  • Delayed recovery: when the current item is slip out of the screen is not immediately, but according to certain delay strategy for recycling, have higher request for delay strategy at this moment, if the delay time is too short is returned to the recycling status immediately, if the delay for a long time can lead to a period of time, memory, and there are a lot of pictures in causing memory leaks. Through the above analysis, it is necessary to take the case of loading multiple graphsDelayed recoveryAnd theAndroidProvides a basis inLRU, or least recently usedThe strategy ofMemory cache technology: LruCacheThe basic idea is to store external objects in the way of strong reference. When the cache space reaches a certain limit, the least recently used objects are released and recycled to ensure that the used cache space is always within a reasonable range.

Its statement is as follows:

/** * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may * become eligible for garbage collection. */
public class LruCache<K.V> {... }Copy the code

From the statement, can learn the achieve LRU methods: internal maintain an orderly queue, when one of the object being accessed is first moved to the team, so as to ensure the use time of objects in the queue is according to the latest from near to far, the team first object is recently used, of the object is the most long before use. Based on this rule, if the cache reaches its limit, the end of the queue is released.

In practice, in order to create a LruCache object, you first determine how much memory the cache can use, which is a determining factor for efficiency. If the cache memory is too small to really work as a cache, you still need to load and recycle resources frequently. If the cache memory is too large, memory overflow may occur. When determining the cache size, consider the following factors:

  • Memory available to the process
  • The size of the resource and the number of resources that need to be displayed on the interface at one time
  • Frequency of resource access

Here is a simple example:

    // Get the maximum amount of memory the process can use
    int maxMemory = (int) Runtime.getRuntime().maxMemory();
    
    mCache = new LruCache<String, Bitmap>(maxMemory / 4) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            returnvalue.getByteCount(); }};Copy the code

In the example, simply set the cache size to 1/4 of the memory the process can use, although in a real project, there are more factors to consider. Note that the sizeOf method is overridden when creating the LruCache object. It returns the sizeOf each object and is used to determine the actual sizeOf the cache and whether the memory limit has been reached.

After the LruCache object is created, if you need to use the resource, you need to fetch the resource from the cache first. If the resource is successfully retrieved, you need to use it directly. Otherwise, you need to load the resource and put it into the cache for future use. To ensure that the loading of resources does not affect application performance, it needs to be carried out in sub-threads, which can be implemented using AsyncTask. Here is the sample code:

    public Bitmap get(String key) {
        Bitmap bitmap = mCache.get(key);
        if(bitmap ! =null) {
            return bitmap;
        } else {
            new BitmapAsyncTask().execute(key);
            return null; }}private class BitmapAsyncTask extends AsyncTask<String.Void.Bitmap> {
        @Override
        protected Bitmap doInBackground(String... url) {
            Bitmap  bitmap = getBitmapFromUrl(url[0]);
            if(bitmap ! =null) {
                mCache.put(url[0],bitmap);
            }
            return bitmap;
        }

        private Bitmap getBitmapFromUrl(String url) {
            Bitmap bitmap = null;
            // Here we get bitmap information from the network using the given URL information.
            returnbitmap; }}Copy the code

In the example, when resources cannot be obtained from the cache, network resources will be loaded according to THE URL information. Currently, the complete code is not provided, and interested students can improve it by themselves.

4. To summarize

In this paper, according to different pictures of loading scenarios presented different loading strategy, to ensure that in the course of loading and now that can meet the demand of the basic display, and will not lead to a memory leak, including on a single image compression, according to local display and for multiple memory cache technology, if a statement is not clear or wrong place, please put forward in time, Everybody study together.