-
Notifications
You must be signed in to change notification settings - Fork 2
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.
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.
Here are the steps that you need to take:
- Set up the "BitmapDownloader" parameters: a typical location where to tune those parameters is the
Application.onCreate()
method. You may define: * theBitmapDownloader.INSTANCES_COUNT
variable to indicate how many instances should be created (defaults to1
), * optionally, theBitmapDownloader.MAX_MEMORY_IN_BYTES
integer array variable to indicate the RAM high water mark that each instance should respect, * optionally, theBitmapDownloader.LOW_LEVEL_MEMORY_WATER_MARK_IN_BYTES
integer array variable to indicate the RAM low water mark that each instance should respect, * very optionally, theBitmapDownloader.IMPLEMENTATION_FQN
string variable which indicates theBitmapDownloader
class Fully Qualified Name, if you have decided to derive from the built-in one. - 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 astatic
way. An already descent implementation is theDownloadInstructions.AbstractInstructions
class, and if you want to define your own, it is very likely that you should derive from it. - Every time you need to bind a bitmap to an Android
android.view.View
(in most cases aandroid.widget.ImageView
), invoke theBitmapDownloader.get()
method from any thread (except if you use the version of the method which takes aboolean
as first argument, and that you set that flagtrue
), and the "BitmapDownloader" will take 1. theview
Android view a bitmap should be bound to, 1. the bitmapimageUid
unique identifier string, 1. some optional-but-very-handyimageSpecs
specifications object, 1. an Androidandroid.os.Handler
(which will be used to run certain methods in the UI thread), 1. and the previously createdDownloadInstructions.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.
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:
- 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 theDownloadInstructions.hasLocalBitmap()
method: if the method answerstrue
, then theDownloadInstructions.onBindLocalBitmap()
method is invoked, and the job ends; - then, it turns the bitmap
imageUid
into a URL, by invoking theDownloadInstructions.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 providedimageSpecs
parameter, you can re-use the sameDonwloadItructions
for various purposes. - 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 ;
- 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 theView
to be temporarily binded to a bitmap). This is done through theDownloadInstructions.hasTemporaryBitmap()
method call: if the method returnstrue
, then theDownloadInstructions.onBindTemporaryBitmap()
is invoked. Whatever, the rest of the job execution continues. - 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-nullInputStream
, it will be used to re-create the bitmap, bind it via theDownloadInstruction.onBindBitmap()
method, and the job ends. This is where you can plug the persistence. - 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 anInputStream
. Otherwise the job ends ; - then, the previously retrieved
InputStream
will be provided to theDownloadInstructions.onInputStreamDownloaded()
method that the "BitmapDownloader" will invoke. This is a placeholder where you may want to persist locally the bitmap ; - then, the previously retrieved
InputStream
needs to be turned into an actualBitmap
. On that purpose, the "BitmapDownloader" invokes theDownloadInstructions.convert()
method: this is a good place to perform graphical operations to the bitmap, if necessary ; - 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 aboolean
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 anull
'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.
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 providedview
parameter isnull
. The parameters enable to get more information. -
DownloadInstructions.onBitmapBound()
: this method will be invoked by the "BitmapDownloader", always after theDownloadInstructions.onBindTemporaryBitmap()/onBindLocalBitmap()/onBindBitmap()
method it has been invoked, to indicate that the binding process is over. The parameters enable to get more information.
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 theBitmapDownloader
instance is running a job ; -
TemporaryAndNoImageSpecs
: this class is aTemporaryImageSpecs
, and holds an alternative Android resource identifier, if the bitmap could not be downloaded ; -
DefaultImageSpecs
: this class is aTemporaryImageSpecs
, and holds a size (anint
) information ; -
OrientedImageSpecs
: this class holds a size (anint
) information, and an orientation flag (aboolean
) ; -
SizedImageSpecs
: this class is aTemporaryImageSpecs
, and holds a width and a height information (twoint
).
The idea behind those built-in classes, is to implement the DownloadInstructions
methods by testing the actual class of the provided imageSpecs
argument.
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 "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 theBitmap
instances: it keeps track on how many times aBitmap
has been requested (every time theBitmapDownloader.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.
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 theBitmapDownloader
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 itscleanUpCache()
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 thePersistence.extractInputStream()
; -
onInputStreamDownloaded()
: return the value of thePersistence.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.
When using the BitmapDownloader.get()
job, the component uses two internal thread pools, in order to run two kinds of commands:
- 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 ;
- 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 theBitmapDownloader
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
andBitmapDownloader.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()
andBitmapDownloader.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.
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:
-
isPreBlocking
: this flag indicates whether the first command of the job should be performed from the calling thread ; -
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).
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.
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.
- You may find a presentation (in French) on SlideShare, which is a bit out-of-date, but which provides the overview and how to get started.
- The [JavaDoc] (http://smartnsoft.github.io/droid4me/javadoc/com/smartnsoft/droid4me/download/BitmapDownloader.html) remains the reference and is your best friend.
- The source code will allow you to understand how it works internally.