Skip to content
Édouard Mercier edited this page May 4, 2016 · 2 revisions

BitmapDownloader

Many Android application do not embed all graphical resources inside the .apk installation package, and need to download efficiently dynamically bitmaps from Internet. Moreover, most of the time, those bitmaps need to be persisted locally, in order to prevent to download them every time they need to be displayed. In some cases, the bitmap need to have a footprint applied to it, when turned into an Android android.graphics.Bitmap instance. In addition, bitmaps also need to be kept in memory via a caching mechanism, because they are very frequently accessed, especially in android.widget.ListView rows, but they should not saturate the memory consumption. At last, the bitmap needs to be binded to its view (typically an ImageView), and it is required to involve that binding at the end of the bitmap retrieval process, and to know the final status of the binding.

Most of the open-source components handle some of those constraints, but not all at the same time. It would be very handy to have a component at hand which addresses all those challenges at the same time.

BitmapDownloader: an all-in-one component for handling Android bitmaps

We have designed the "BitmapDownloader" component with those requirements in mind, and we have strived to make it both efficient and flexible. Because that kind of component is very critical, we have introduced a thin layer of abstraction, so that it can be unitary-tested (the unitary tests are available in the project).

The BitmapDownloader component proposes multiple instances, so as to provide a separate and independent caching feature. Its interface is very simple, since it also proposes to retrieve a bitmap and attach it to a android.view.View and to clear the memory cache.

The "BitmapDownloader" works closely with a DownloadInstructions.Instructions interface, which will be used throughout the bitmap retrieval and attaching process so as to provide instructions.

How to use the "BitmapDownloader" component

Here are the steps that you need to take:

  1. Set up the "BitmapDownloader" parameters: a typical location where to tune those parameters is the Application.onCreate() method. You may define: * the BitmapDownloader.INSTANCES_COUNT variable to indicate how many instances should be created (defaults to 1), * optionally, the BitmapDownloader.MAX_MEMORY_IN_BYTES integer array variable to indicate the RAM high water mark that each instance should respect, * optionally, the BitmapDownloader.LOW_LEVEL_MEMORY_WATER_MARK_IN_BYTES integer array variable to indicate the RAM low water mark that each instance should respect, * very optionally, the BitmapDownloader.IMPLEMENTATION_FQN string variable which indicates the BitmapDownloader class Fully Qualified Name, if you have decided to derive from the built-in one.
  2. Create an implementation of the DownloadInstructions.Instructions interface: most of the time, you will need only one instance of that interface, which you can define in a static way. An already descent implementation is the DownloadInstructions.AbstractInstructions class, and if you want to define your own, it is very likely that you should derive from it.
  3. Every time you need to bind a bitmap to an Android android.view.View (in most cases a android.widget.ImageView), invoke the BitmapDownloader.get() method from any thread (except if you use the version of the method which takes a boolean as first argument, and that you set that flag true), and the "BitmapDownloader" will take 1. the view Android view a bitmap should be bound to, 1. the bitmap imageUid unique identifier string, 1. some optional-but-very-handy imageSpecs specifications object, 1. an Android android.os.Handler (which will be used to run certain methods in the UI thread), 1. and the previously created DownloadInstructions.Instructions instance.

The component will handle everything for you, and in particular, if the bitmap is already in memory, no HTTP request will be performed.

The "BitmapDownloader" job workflow

To ease the explanation, we will refer to the "job" term to refer to the action of invoking the BitmapDownloader.get(): this job is defined by the couple of the android.view.View and bitmapUid provided as parameters.

When you invoke the BitmapDownloader.get() method, i.e. run a "job", the "BitmapDownloader" follows this overall workflow:

  1. first, it asks the DownloadInstructions if it can provide the bitmap "locally", that is to say from a resource bundled in the application .apk. This is done by invoking the DownloadInstructions.hasLocalBitmap() method: if the method answers true, then the DownloadInstructions.onBindLocalBitmap() method is invoked, and the job ends;
  2. then, it turns the bitmap imageUid into a URL, by invoking the DownloadInstructions.computeUrl() method. The fact to turn the bitmap unique identifier into an actual URL is very convenient, because it enables you to centralize in one place that computation. It may appear overkill at first sight, but with the help of the additionally provided imageSpecs parameter, you can re-use the same DonwloadItructions for various purposes.
  3. then, it looks whether the bitmap asks for would not be already in memory, based on the previously computed URL. If this is the case, the binding is run and the job completes ;
  4. then, it asks the DownloadInstructions whether a temporary bitmap is available while the rest of the job is executed (which may take time, and we'd prefer the View to be temporarily binded to a bitmap). This is done through the DownloadInstructions.hasTemporaryBitmap() method call: if the method returns true, then the DownloadInstructions.onBindTemporaryBitmap() is invoked. Whatever, the rest of the job execution continues.
  5. at this stage, we know that the bitmap is not in memory cache. It then asks if the bitmap could not be retrieved from a local persistent instance: this is done by invoking the DownladInstructions.getInputStream() method: if the method returns a non-null InputStream, it will be used to re-create the bitmap, bind it via the DownloadInstruction.onBindBitmap() method, and the job ends. This is where you can plug the persistence.
  6. at this stage, we know that the bitmap is neither available in memory cache, nor in the local persistent: the component needs to download it. It invokes the DownladInstructions.downloadInputStream(): if this method succeeds, it returns an InputStream. Otherwise the job ends ;
  7. then, the previously retrieved InputStream will be provided to the DownloadInstructions.onInputStreamDownloaded() method that the "BitmapDownloader" will invoke. This is a placeholder where you may want to persist locally the bitmap ;
  8. then, the previously retrieved InputStream needs to be turned into an actual Bitmap. On that purpose, the "BitmapDownloader" invokes the DownloadInstructions.convert() method: this is a good place to perform graphical operations to the bitmap, if necessary ;
  9. at last, the DownloadInstruction.onBindBitmap() method is invoked with the extracted bitmap and the job ends.

The strategy used internally during that workflow is a kind of last-in-first-out (LIFO) when serving the jobs, because, in the case the component is used in a ListView, we prefer to have the lastly displayed rows properly updated graphically, instead of the ones which have already disappeared from the screen because the ListView has already been scrolled vertically. This policy has shown that it is a good choice, when confronted to the reality.

Three major things:

  • When using the BitmapDownloader.get() method which takes a boolean as a first argument, it is supposed that the caller is the UI thread, and the component attempts to perform some optimizations, which will be effective if the bitmap asks for is already in RAM.
  • Some of the DownloadInstructions method callbacks will be run by the "BitmapDownloader" in the UI thread (only the ones, which require that): it is very important that those methods execute as fast as possible, in order to prevent from a non responsive User Interface!
  • You can use the BitmapDownloader.get() method with a null 'view` argument: no binding will be obviously run, but the bitmap will be extracted from the local persistent and put in cache, or donwloaded from the network and then put in cache. This means that you can use the "BitmapDownloader" component to pre-load bitmaps.

The DownloadInstructions notifications

As we have seen, the DownloadInstructions is requested so as to guide the bitmap "job", but the BitmapDownloader also notifies during the job execution workflow. Here are the methods which may be invoked:

  • DownloadInstructions.onBitmapReady(): this method will be invoked by the "BitmapDownloader" to indicate when the bitmap is eventually available at the end of the job. It is even invoked when the provided view parameter is null. The parameters enable to get more information.
  • DownloadInstructions.onBitmapBound(): this method will be invoked by the "BitmapDownloader", always after the DownloadInstructions.onBindTemporaryBitmap()/onBindLocalBitmap()/onBindBitmap() method it has been invoked, to indicate that the binding process is over. The parameters enable to get more information.

Some built-in imageSpecs classes

You will find in the DownloadSpecs container class, some convenient classes, which helps to implements the DownloadInstruction interface:

  • TemporaryImageSpecs: this class holds a reference to the Android resource identifier where a temporary bitmap may be extracted from, while the rest of the BitmapDownloader instance is running a job ;
  • TemporaryAndNoImageSpecs: this class is a TemporaryImageSpecs, and holds an alternative Android resource identifier, if the bitmap could not be downloaded ;
  • DefaultImageSpecs: this class is a TemporaryImageSpecs, and holds a size (an int) information ;
  • OrientedImageSpecs: this class holds a size (an int) information, and an orientation flag (a boolean) ;
  • SizedImageSpecs: this class is a TemporaryImageSpecs, and holds a width and a height information (two int).

The idea behind those built-in classes, is to implement the DownloadInstructions methods by testing the actual class of the provided imageSpecs argument.

Notifying about Internet connectivity

In order to save the device CPU and battery, and to speed up a BitmapDownloader job, the BitmapDownloader.setConnected() method enables to indicate in real time whether the device running the hosting application has Internet connectivity. When it is false, the Instruction.downloadInputStream() method will not be invoked when the BitmapDownloader detects that it needs to download the bitmap. If you listen to the device connectivity and plug that real-time information to all BitmapDownloader instances, this will optimize their behavior.

The memory water marks

The "BitmapDownloader" lets you set, for each instance, define how much memory, at most, it should consumes for its caching, via a traditional memory high/low-level water mark mechanism. As explained above, you may fine-tune two parameters:

  • the high-level water mark: the component monitors the space consumed by each 'Bitmap' in memory, and when this upper limit is reached, a cleaning process is run, so as to free memory ;
  • the low-level water mark: when clearing the cache, as many Bitmap instances will be removed from memory as necessary, so that this low-level memory consumption is reached. However, the component does not discard from memory randomly the Bitmap instances: it keeps track on how many times a Bitmap has been requested (every time the BitmapDownloader.get() method is invoked, so as to discard the ones that have less been requested. This clearing strategy may be opened in the future...

You may want to empty the cache yourself: on that purpose, you may invoke the empty() method.

The AnalyticsListener for listening to the BitmapDownloader internal state

When using multiple BitmapDownloader instances in a picture very demanding application, it is very often important to be able to tune properly the cache memory water marks:

  • too large high level water marks will result in many OutOfMemoryError situations: even if the BitmapDownloader is supposed to gracefully support those cases, this causes repeating garbage collections, incurring CPU consumption, and a feeling that the hosting application GUI is hanging, or lagging ;
  • too small high-level water marks, or too low low-level water marks will result in the BitmapDownloader to invoke too often its cleanUpCache() method: this will cause memory-cached bitmaps to be flushed, and them to be fetched back later on when displaying them, from either the local persistence or from the Internet once again, which obviously will induce either battery usage in one case, and network consumption in the other case, and in all cases a CPU waste and a lagging application GUI.

This is where the AnalyticsListener comes in hand: this interface exposes a single method, which will be invoked every time the BitmapDownloader internal state changes. You may set an implementation of this interface though the BitmapDownloader.ANALYTICS_LISTENER static attribute ; by default, this attribute is set to null. The interface onAnalytics() method will be invoked by providing an AnalyticsData Plain Old Java Object, which contains information about the BitmapDownloader current internal state. This method will be invoked in the following cases:

  • the BitmapDownloader.cleanUpCache() has end-up its processing ;
  • a new bitmap has been promoted to the cache memory ;
  • an OutOfMemoryError exception has occurred.

Those events are very handy, because they provided AnalyticsData POJO reveals:

  • the number of bitmaps currently in the cache,
  • the number of times the BitmapDownloader.cleanUpCache() method has been invoked since its inception,
  • and the number of OutOfMemoryError exceptions have been detected since its inception.

Note that you may access to the cache current memory usage by invoking the BitmapDownloader.getMemoryConsumptionInBytes() method. This is especially handy when fine-tuning the cache water mark levels.

How to couple the "BitmapDownloader" with the "Persistence" component

If you want the downloaded bitmaps to be persisted, implement the following methods of the DownloadInstructions.AbstractInstructions class:

  • getInputStream(): return the value of the Persistence.extractInputStream() ;
  • onInputStreamDownloaded(): return the value of the Persistence.flushInputStream().

This will ensure that, when the bitmap is not already in memory cache, a first attempt to retrieve from the persistence layer will be performed, and once the bitmap has been downloaded, it will be persisted.

Some noteworthy BitmapDownloader internal implementation details, and fine tuning

When using the BitmapDownloader.get() job, the component uses two internal thread pools, in order to run two kinds of commands:

  1. a first "pre"-command, which queries the memory whether a bitmap is available: in that case, the binding is done. The first threads pool is used to run that command. All operations are done in memory, hence, the execution of the job execution is very fast, and typically below the millisecond ;
  2. a second and final "download"-command which is responsible for generating the bitmap from a local persistence, if any, and them from Internet if necessary. The second threads pool is used to run that command. If the BitmapDownloader is bound to a local persistence cache, that command will perform storage access operations, which are CPU, battery and time consuming. When that command needs to download the bitmap bytes, it consumes battery, CPU and network band. This command is hence a magnitude more expensive than the previous one, and this is the reason why the BitmapDownloader must be properly fine-tuned..

In order to tune the BitmapDownloader, here are some key key parameters to set.

  • The number of instances: this is set through the BitmapDownlader.INSTANCES_COUNT static attribute. When designing the application which resorts to this component, it is important to define logical bitmaps units, depending on their physical dimensions, the screens they are displayed on, the number of times they are accessed, the end-user experience. Each instance is independent of the others in terms of memory management, even if all instances share the same two internal pre and download-command threads pools. Declaring multiple instances does not cause significant memory overhead.
  • For each instance, its memory water marks: when using multiple instances, depending of the nature of their bitmaps, mostly regarding their dimensions, it is important to properly tune those water markers through the BitmapDownloader.HIGH_LEVEL_MEMORY_WATER_MARK_IN_BYTES and BitmapDownloader.LOW_LEVEL_MEMORY_WATER_MARK_IN_BYTES static attributes.
  • The number of core threads inside the pre and download threads pool which are customizable through the BitmapDownloader.setPreThreadPoolSize() and BitmapDownloader.setDownloadThreadPoolSize() static methods.

As a bonus track, you may use the BitmapDownloader.AnalyticsDisplayer component, which implements the AnalyticsListener interface, and exposes an Android View which reports graphical jauges about each BitmapDownloader instance. Displaying graphically the state of the various BitmapDownloader instances in real time teaches the developer a lot about how each instance should be tuned.

Running a synchronous BitmapDownloader.get() job

If, for some reason like automatically downloading some content for further off-line access, you need to download a bitmap in a synchronous way, the BitmapDownloader component offers an alternate get() method for running that job, which takes two boolean first arguments:

  1. isPreBlocking: this flag indicates whether the first command of the job should be performed from the calling thread ;
  2. isDownloadBlocking: this flag indicates whether the second command of the job, if any, should executer in the same thread as the calling job thread.

If you invoke the get(true, true, null, ...) job, you are ensured that the whole download and binding job is done from the current thread, and this enables to pre-fetch the bitmap, and have it available in the cache (persistence and/or memory).

Using your own BitmapDownloader implementation

The design of the BitmapDownloader is open, and you are free to use whatever implementation by setting the BitmapDownloader.IMPLEMENTATION_FQN static attribute.

If you are not happy with the current CoreBitmapDownloader implementation, which defaults to the BitmapDownloader, it is up to you to derive from the BitmapDownloader and, for instance, override the BitmapDownloader.cleanUpCacheInstance() method.

Conclusion

In a stressed environment, it is important that the component behaves properly, and we have done our best to handle this situation properly. The component takes great care about:

  • optimization: invoking many times that component, and especially for the same View for different URLs or for the same URL, should not incur useless operations, like retrieving the actual bitmap from the persistence, from the Internet, like binding the bitmap to the view ;
  • end-user experience: no matter how the end-user interacts with the User Interface, his screen should be always as much as possible up-to-date regarding the displayed images.
  • memory conservation: because an application may display a huge bunch of bitmaps, it is very important that it does not get saturated by its caching system. The component can be split into several instances, and each instance can state how it should behave regarding the memory consumption, through a memory low/high water mark mechanism.

Additional Resources