This is actually a problem I encountered in the interview a few days ago. Although I have analyzed the source code of Glide before, to be honest, if it was not for this kind of problem in the interview, I would not pay attention to the Bitmap reuse of Glide. Anyway, with this problem, let’s see how Glide implements Bitmap reuse

1. Pooling and object reuse

In fact, there are several examples of “pooling” and object reuse in Android. A typical example is Message in Handler. When we obtain a Message using Message obtain, we actually obtain the Message from the Message pool. Messages in handlers are data structures maintained through linked lists to form a “Message pool.” The maximum number of pools is specified by the MAX_POOL_SIZE parameter, which is 50.

So what are the benefits of “pooling” and object reuse?

This is because for frequently used objects such as Message, if you create one object each time you use it, you can cause virtual machine GC due to frequent creation and destruction, resulting in page stuttering, especially on low-end devices. After “pooling”, the created objects are retrieved from the pool each time for reuse, thus avoiding frequent VIRTUAL machine GC.

For image-related, memory intensive objects such as Bitmaps, frequent creation and destruction can have a much greater impact on the virtual machine than Message, so Bitmap reuse is important.

2. Start with Bitmap recycling

Let’s take a look at how bitmaps are recycled.

In Android 2.3 and later versions, use recycle() to recycle memory to prevent OOM. However, using this method requires ensuring that the bitmap is no longer in use, or recycling it will cause a runtime error. Therefore, the official recommendation is to count bitmap references by reference count, and only call this method when the bitmap is no longer referenced.

The official document reference: developer.android.com/topic/perfo…

On the Android 3.0 introduces BitmapFactory. Options. InBitmap fields. If this option is set, decoding methods that take the Options object will try to reuse the existing bitmap when loading content. This allows existing bitmaps to be reused, reducing object creation and thus reducing the probability of GC occurring. However, there are some limitations to how inBitmap can be used. Especially prior to Android 4.4 (API level 19), the system only supported bitmaps of the same size. After Android 4.4, bitmaps can be reused as long as the memory size is not smaller than the required Bitmap.

Therefore, when we need to use Bitmap in Android, we should consider using Bitmap reuse to improve application performance. But how do you encapsulate this complex logic? The official recommendation is to use a mature image loading framework such as Glide. So, let’s take a look at how Glide implements Bitmap reuse.

Glide’s BitmapPool

Let’s start our analysis directly with Glide’s BitmapPool. BitmapPool is an interface defined as follows:

public interface BitmapPool {
  long getMaxSize(a);
  void setSizeMultiplier(float sizeMultiplier);
  // Insert bitmap into pool for reuse
  void put(Bitmap bitmap);
  // Get a bitmap from the pool for reuse
  @NonNull Bitmap get(int width, int height, Bitmap.Config config);
  @NonNull Bitmap getDirty(int width, int height, Bitmap.Config config);
  void clearMemory(a);
  void trimMemory(int level);
}
Copy the code

BitmapPool Allows users to reuse Bitmap objects by defining a Pool. In Glide, BitmapPool has a default implementation LruBitmapPool. As the name suggests, it is also based on the concept of LRU.

As we mentioned earlier, inBitmap is divided into Android 4.4, and there are differences between previous and later versions of inBitmap. How does BitmapPool handle this difference? The answer is the strategic model. Glide defines the LruPoolStrategy interface, which internally defines operations related to add and delete. Real Bitmap data is stored in LruPoolStrategy based on mapping relationships such as size and color. BitmapPool get and PUT are also done through LruPoolStrategy get and PUT.

interface LruPoolStrategy {
  void put(Bitmap bitmap);
  @Nullable Bitmap get(int width, int height, Bitmap.Config config);
  @Nullable Bitmap removeLast(a);
  String logBitmap(Bitmap bitmap);
  String logBitmap(int width, int height, Bitmap.Config config);
  int getSize(Bitmap bitmap);
}
Copy the code

LruPoolStrategy provides three implementations by default, namely AttributeStrategy, SizeConfigStrategy, and SizeStrategy. AttributeStrategy applies to Android 4.4 or later, and SizeConfigStrategy and SizeStrategy apply to Android 4.4 or later.

AttributeStrategy uniquely identifies a Bitmap with three parameters: Width, height, and config (ARGB_8888, etc.). When retrieving a Bitmap, only these three conditions match exactly. SizeConfigStrategy uses Size (the total number of pixels in an image) and Config as unique identifiers. When obtaining the Bitmap matched by Cofig (generally the same config), then ensure that the size of the Bitmap is larger than our expected size and smaller than 8 times the expected size, then reuse it (possibly to save memory space).

LRU is implemented by BitmapPool through LruPoolStrategy. After putting data into BitmapPool, the following operations are performed to adjust the size of the BitmapPool:

private synchronized void trimToSize(long size) {
    while (currentSize > size) {
        // Remove the tail
        final Bitmap removed = strategy.removeLast();
        if (removed == null) {
            currentSize = 0;
            return;
        }
        currentSize -= strategy.getSize(removed);
        // ...
        / / recyclingremoved.recycle(); }}Copy the code

4. Bitmap loading and reuse

Let’s review the general Bitmap loading steps. The normal image loading process is as follows,

// Set inJustDecodeBounds to true to get the image size
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);

// Set inJustDecodeBounds to false to actually load
options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
options.inJustDecodeBounds = false;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
Copy the code

That is, first of all, by setting options. InJustDecodeBounds to true to get the size of the real images, in order to set the sampling rate. This is because we do not load all the pixels of the image directly. Instead, we load the image on demand after sampling to reduce the memory footprint of the image. When really need to load, set the options. InJustDecodeBounds to false, then call related to decode method.

So how does Bitmap reuse work? Decode a Bitmap object by specifying the inBitmap parameter of options at load time:

options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
Copy the code

How does Glide load Bitmap

The previous analysis of Glide source, pay attention to the whole process, for many details did not take care of, here I simplify the logic. First, Glide’s Bitmap loading process resides in the Downsampler class. Images are loaded when an InputStream is retrieved from another source, such as the network or disk. Here is the decodeFromWrappedStreams method for Downsampler. Here is the process to perform the image load. The main code logic and functionality has been noted in the comments:

  private Bitmap decodeFromWrappedStreams(InputStream is, BitmapFactory.Options options, DownsampleStrategy downsampleStrategy, DecodeFormat decodeFormat, ...) throws IOException {
    long startTime = LogTime.getLogTime();
    // Set inJustDecodeBounds to read the original size of the image
    int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool);
    int sourceWidth = sourceDimensions[0];
    int sourceHeight = sourceDimensions[1];
    String sourceMimeType = options.outMimeType;

    // ...

    // Read the exif information of the image and rotate the image if necessary
    int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool);
    int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
    boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);
    int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
    int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;

    ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);

    // Calculate the image size and config as required, and set the calculation result directly to optionscalculateScaling(imageType, is, ... , options); calculateConfig(is, ... , options, targetWidth, targetHeight);boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
      // ...
      // Get a Bitmap from the BitmapPool according to the expected size of the image for reuse
      if (expectedWidth > 0 && expectedHeight > 0) { setInBitmap(options, bitmapPool, expectedWidth, expectedHeight); }}// Start decode logic
    Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
    callbacks.onDecodeComplete(bitmapPool, downsampled);

    / /... Image rotation and other subsequent logic

    return rotated;
  }
Copy the code

In the setInBitmap method in the above code, the get method of BitmapPool is called to obtain the reused Bitmap object, and its code is as follows:

  private static void setInBitmap(
      BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
    @Nullable Bitmap.Config expectedConfig = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      if (options.inPreferredConfig == Config.HARDWARE) {
        return;
      }
      expectedConfig = options.outConfig;
    }
    if (expectedConfig == null) {
      expectedConfig = options.inPreferredConfig;
    }
    // inBitmap was called
    options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
  }
Copy the code

In addition, by checking the inBitmap document notes of Bitmap, we can see that there may be some cases that lead to abnormalities in the inBitmap process, so will Glide cause abnormalities in the loading process due to the reuse of Bitmap? How is Glide handled? Referring to the above code, we can see that loading the image calls a method called decodeStream. After my simplification, the method is roughly as follows:

  private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options, DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException {
    // ...
    final Bitmap result;
    TransformationUtils.getBitmapDrawableLock().lock();
    try {
      // Data loading
      result = BitmapFactory.decodeStream(is, null, options);
    } catch (IllegalArgumentException e) {
      // ...
      if(options.inBitmap ! =null) {
        try {
          // Input stream reset
          is.reset();
          bitmapPool.put(options.inBitmap);
          // Clean up inBitmap and load it a second time
          options.inBitmap = null;
          // Call load again
          return decodeStream(is, options, callbacks, bitmapPool);
        } catch (IOException resetException) {
          throwbitmapAssertionException; }}throw bitmapAssertionException;
    } finally {
      TransformationUtils.getBitmapDrawableLock().unlock();
    }
    if (options.inJustDecodeBounds) {
      is.reset();
    }
    return result;
  }
Copy the code

That is, Glide first loads images by setting up inBitmap to reuse them. If an exception occurs, the inBitmap will not be empty, so the exception will be handled, the inBitmap will be cleaned up, and the decodeStream method will be called again, which will not be used by Bitmap reuse. Therefore, Glide will reuse Bitmap internally through error retry mechanism, and when reuse and error occurs, it will be downgraded to a non-reuse way for the second load.

conclusion

The above is the principle of Glide Bitmap reuse, I hope this article will help you!