SoFunction
Updated on 2025-04-10

Detailed explanation of how to efficiently load pictures on Android

1. Overview

In the design of Android applications, it is almost inevitable that images need to be loaded and displayed. Since different images vary greatly in size, some images may only require dozens of KB of memory space, while some images need to occupy dozens of MB of memory space; or one image does not need to occupy too much memory, but multiple images need to be loaded and displayed at the same time.

In these cases, loading images requires a large amount of memory, and the memory space allocated to each process by the Android system is limited. If the memory required for the loaded images exceeds the limit, the process will experience OOM, that is, memory overflow.

This article uses different loading methods for two different scenarios such as loading large images or loading multiple images at a time to avoid possible memory overflow problems.

I won't say much below, let's take a look at the detailed introduction

2. Load large pictures

Sometimes the loading and display of an image requires a lot of memory. For example, the size of the image is 2592x1936, and the bitmap configuration used is ARGB_8888, and the required size in memory is 2592x1936x4 bytes, which is about 19MB. Just loading such an image may exceed the process's memory limit, resulting in memory overflow, so it will definitely not be loaded directly into memory during actual use.

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

  • Show all contents of a picture: compress the original picture.
  • Show part of the content of a picture: partially display the original picture.

2.1 Picture compression display

The compressed display of pictures refers to compressing the length and width of the original picture to reduce the memory usage of the picture, so that it can be displayed normally in the application, and at the same time ensure that there will be no memory overflow during loading and displaying.

BitmapFactory is a tool class for creating Bitmap objects. It can use data from different sources to generate Bitamp objects. During the creation process, you can also configure and control the objects that need to be generated. The class declaration of BitmapFactory is as follows:

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

Since the image size cannot be predicted in advance before loading the picture, it is necessary to decide whether the image needs to be compressed based on the size of the picture and the memory status of the current process before actually loading. If the memory space required to load the original picture has exceeded the memory size that the process intends to provide or can provide, compressed images must be considered.

2.1.1 Determine the length and width of the original picture

Simply put, compressing the picture means reducing the length and width of the original picture at a certain scale, so first of all, you must determine the length and width information of the original picture. In order to obtain the length and width information of the image, use the (Resources res, int id, Options opts) interface, which is declared 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  and )
 * @throws IllegalArgumentException if {@link #inPreferredConfig}
 *  is {@link #HARDWARE}
 *  and {@link #inMutable} is set, if the specified color space
 *  is not {@link #RGB RGB}, or if the specified color space's transfer
 *  function is not an {@link  ICC parametric curve}
 */
 public static Bitmap decodeResource(Resources res, int id, Options opts) {

Through this function declaration, you can see that the length and width information of the picture can be obtained through this interface. At the same time, since returning null does not apply for memory space, unnecessary memory applications are avoided.

In order to obtain the length and width information of the image, an Options parameter must be passed, with the inJustDecodeBounds set to true, and its declaration is as follows:

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

The following is a sample code for obtaining image length and width information:

  options = new ();
 // Specify that when parsing image files, only edge information is parsed without creating bitmap objects.  = true;
 // is a test image resource file using 2560x1920. (getResources(), , options);
 int width = ;
 int height = ;
 (TAG, "width: " + width + ", height: " + height);

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

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

2.1.2 Determine the target compression ratio

After learning the length and width information of the original picture, in order to be able to perform subsequent compression operations, the target compression ratio must be determined first. The so-called compression ratio refers to the cropping ratio of the original length and width. If the original picture is 2560x1920, the compression ratio is 4, and the compressed picture is 640x480, and the final size is 1/16 of the original picture.

The corresponding attribute in the compression ratio is 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;

It should be noted that inSampleSize can only be a power of 2. If the incoming value does not meet the conditions, the decoder will select a power of 2 that is the most frugal to the incoming value; if the incoming value is less than 1, the decoder will directly use 1.

To determine the final compression ratio, you must first determine the target size, that is, the length and width information of the compressed target picture, and select the most suitable compression ratio based on the original length and width and the target length and width. The following is a sample code:

 /**
 * @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;
 }

It should be noted that desWidth and desHeight are the maximum length and width values ​​of the target image, rather than the final size, because the compression ratio determined by this method will ensure that the final length and width of the image are not greater than the target value.

In actual tests, set the original image size to 2560x1920 and the target image size to 100x100:

 int sampleSize = (2560, 1920, 100, 100);
 (TAG, "sampleSize: " + sampleSize);

The test results are as follows:

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

The final compression ratio is 32. If you use this ratio to compress the 2560x1920 picture, you will finally get an 80x60 picture.

2.1.3 Compressed pictures

In the first two parts, the length and width information of the original picture and the target compression ratio are determined respectively. In fact, determining the length and width of the original picture is also to obtain the compression ratio. Since the compression comparison has been obtained, the actual compression operation can be performed. You only need to pass the obtained inSampleSize to (Resources res, int id, Options opts) through Options.

Here is the sample code:

 public static Bitmap compressBitmapResource(Resources res, int resId, int inSampleSize) {
  options = new ();
  = false;
  = inSampleSize;
 return (res, resId, options);
 }

2.2 Local display of pictures

Image compression will affect the quality and display effect to a certain extent. It is not desirable in some scenarios. For example, when displaying a map, you must have high-quality pictures. At this time, compression processing cannot be performed. In this scenario, it is not necessary to display all parts of the picture at once. You can consider loading and displaying only specific parts of the picture at once, that is, local display ***.

To achieve local display effect, you can use BitmapRegionDecoder to implement it. It is used to display specific parts of the picture, especially in the scene where the original picture is very large and cannot be loaded into memory at once. 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.
 *
 * <p>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 { ... }

This also explains that if you use BitmapRegionDecoder for local display: first create an instance through newInstance(), then use decodeRegion() to create a Bitmap object on the picture memory of the specified area, and then display it in the display control.
Pass

Create a parser instance through(), and 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 .VERSION_CODES#KITKAT},
 * if {@link InputStream#markSupported ()} returns true,
 * <code>(1024)</code> would be called. As of
 * {@link .VERSION_CODES#KITKAT}, this is no longer the case.</p>
 */
 public static BitmapRegionDecoder newInstance(InputStream is,
  boolean isShareable) throws IOException { ... }

It should be noted that this is just one of the newInstance functions of BitmapRegionDecoder. In addition, there are other implementation forms. Readers who are interested can check it out by themselves.

After creating the BitmapRegionDecoder instance, you can call the decodeRegion method to create a local Bitmap object, and its function declaration is as follows:

 /**
 * 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 #inPreferredConfig}
 *  is {@link #HARDWARE}
 *  and {@link #inMutable} is set, if the specified color space
 *  is not {@link #RGB RGB}, or if the specified color space's transfer
 *  function is not an {@link  ICC parametric curve}
 */
 public Bitmap decodeRegion(Rect rect,  options) { ... }

Since this part is relatively simple, the following is the relevant example code:

 // Analyze the length and width values ​​of the original image to facilitate the specification of the area to be displayed when performing local display later.  options = new ();
  = true;
 (getResources(), , options);
 int width = ;
 int height = ;

 try {
 // Create a local parser InputStream inputStream = getResources().openRawResource();
 BitmapRegionDecoder decoder = (inputStream,false);
 
 // Specify the rectangular area to be displayed, the upper left 1/4 area of ​​the original image to be displayed here. Rect rect = new Rect(0, 0, width / 2, height / 2);

 // Create a bitmap configuration, using RGB_565 here, each pixel takes up 2 bytes.  regionOptions = new ();
  = .RGB_565;
 
 // Create a Bitmap object that gets the specified area and displays it. Bitmap regionBitmap = (rect,regionOptions);
 ImageView imageView = (ImageView) findViewById(.main_image);
 (regionBitmap);
 } catch (Exception e) {
 ();
 }

Judging from the test results, it is indeed only displayed in the image content in the upper left 1/4 area of ​​the original image, and the results will no longer be posted here.

3. Load multiple pictures

Sometimes multiple images need to be displayed in the application at the same time. For example, when using ListView, GridView and ViewPager, you may need to display one image on each item. This will become more complicated because you can change the visible items of the control by sliding. If you add a visible item, a picture will be loaded, and the pictures of the invisible items will continue to be in memory, which will cause memory overflow as it continues to increase.

In order to avoid the memory overflow problem in this case, it is necessary to recycle the image resources corresponding to the invisible item, that is, when the current item is slided out of the display area of ​​the screen, the relevant pictures are considered. At this time, the recycling strategy has a great impact on the performance of the entire application.

  • Recycle now: Recycle image resources immediately when the current item is slided out of the screen, but if the slided out item is quickly slided into the screen, the image needs to be reloaded, which will undoubtedly lead to a degradation of performance.
  • Delay recovery: When the current item is slided out of the screen, it is not recycled immediately, but is recycled according to a certain delay strategy. At this time, there are high requirements for the delay strategy. If the delay time is too short, it will return to the immediate recovery state. If the delay time is long, it may cause a large number of pictures in the memory for a period of time, which will cause memory overflow. Through the above analysis, delayed recycling must be adopted for loading multiple graphs. Android provides a memory caching technology based on LRU, that is, the least used strategy recently: LruCache, its basic idea is to save external objects in a strong reference form. When the cache space reaches a certain limit, the least used objects are released and recycled to ensure that the cache space used 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> { ... }

From the declaration, we can understand how it implements LRU: it maintains an ordered queue internally, and whenever one of the objects is accessed, it is moved to the head of the queue, which ensures that the objects in the queue are arranged from near to far according to the recent use time, that is, the object at the head of the queue is used recently, and the object at the tail of the queue is used the longest ago. It is precisely based on this rule that if the cache reaches the limit, just release the tail object directly.

In actual use, in order to create an LruCache object, the first thing to do is to determine the memory size that the cache can use, which is a decisive factor in efficiency. If the cache memory is too small to truly play the role of the cache, resources still need to be loaded and recycled frequently; if the cache memory is too large, it may cause memory overflow. When determining the cache size, the following factors should be combined:

  • Memory that can be used by the process
  • The size of the resource and the number of resources that need to be displayed on the interface at once
  • Resource access frequency

Here is a simple example:

 // Get the maximum amount of memory that the process can use int maxMemory = (int) ().maxMemory();
 
 mCache = new LruCache&lt;String, Bitmap&gt;(maxMemory / 4) {
  @Override
  protected int sizeOf(String key, Bitmap value) {
   return ();
  }
 };

In the example, simply set the cache size to 1/4 of the memory that the process can use, of course, there will be more factors to consider in actual projects. It should be noted that when creating LruCache objects, the sizeOf method needs to be rewrite, which is used to return the size of each object, is used to determine the actual size of the current cache and determine whether the memory limit has been reached.

After creating the LruCache object, if you need to use the resource, first go to the cache to retrieve it. If it is successfully retrieved, use it directly. Otherwise, the resource will be loaded and put into the cache to facilitate the next use. In order to load the resource behavior will not affect the application performance, it needs to be performed in the child thread, which can be implemented using AsyncTask.

Here is the sample code:

 public Bitmap get(String key) {
  Bitmap bitmap = (key);
  if (bitmap != null) {
   return bitmap;
  } else {
   new BitmapAsyncTask().execute(key);
   return null;
  }
 }

 private class BitmapAsyncTask extends AsyncTask&lt;String, Void, Bitmap&gt; {
  @Override
  protected Bitmap doInBackground(String... url) {
   Bitmap bitmap = getBitmapFromUrl(url[0]);
   if (bitmap != null) {
    (url[0],bitmap);
   }
   return bitmap;
  }

  private Bitmap getBitmapFromUrl(String url) {
   Bitmap bitmap = null;
   // Here we need to use the given url information to obtain bitmap information from the network.   return bitmap;
  }
 }

In the example, when resources cannot be obtained from the cache, network resources will be loaded based on the url information. There is no complete code given at present. Interested students can improve it themselves.

4. Summary

This article mainly proposes different loading strategies for different image loading scenarios to ensure that the basic display needs can be met during the loading and display process without causing memory overflow. Specifically, it includes compressed display for a single image, local display and memory caching technology for multiple images. If there are any unclear statements or even errors, please propose them in time and learn together.

Okay, the above is the entire content of this article. I hope that the content of this article has a certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support.