I have read several topics on lazy list loading in stackoverflow and I am trying to understand how to work on the different cache levels in android.
As mentioned here:
Lazy load of images in ListView
I used the
Multithreading For Performance, a tutorial by Gilles Debunne.
example. I modified it in order to just work with the correct way and also work with 1.6. Here is the code:
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public class ImageDownloader
{
private static final String TAG = "ImageDownloader";
private static final int HARD_CACHE_CAPACITY = 40;
private static final int DELAY_BEFORE_PURGE = 10 * 1000; // in milliseconds
private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(HARD_CACHE_CAPACITY / 2);
/* Download the specified image from the Internet and binds it to the provided ImageView. The
* binding is immediate if the image is found in the cache and will be done asynchronously
* otherwise. A null bitmap will be associated to the ImageView if an error occurs.
*
* @param url The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void download(String url, ImageView imageView) {
Log.d(TAG, "download(String url, ImageView imageView)");
resetPurgeTimer();
Bitmap bitmap = getBitmapFromCache(url);
if (bitmap == null) {
Log.d(TAG, "(bitmap == null)");
forceDownload(url, imageView);
} else {
Log.d(TAG, "(bitmap != null) ");
cancelPotentialDownload(url, imageView);
imageView.setImageBitmap(bitmap);
}
}
/*
* Same as download but the image is always downloaded and the cache is not used.
* Kept private at the moment as its interest is not clear.
private void forceDownload(String url, ImageView view) {
forceDownload(url, view, null);
}
*/
/**
* Same as download but the image is always downloaded and the cache is not used.
* Kept private at the moment as its interest is not clear.
*/
private void forceDownload(String url, ImageView imageView) {
Log.d(TAG, "forceDownload(String url, ImageView imageView)");
// State sanity: url is guaranteed to never be null in DownloadedDrawable and cache keys.
if (url == null) {
Log.d(TAG, "(url == null)");
imageView.setImageDrawable(null);
return;
}
Bitmap bitmap = null;
BitmapDownloaderTask task = null;
if (cancelPotentialDownload(url, imageView)) {
Log.d(TAG, "(cancelPotentialDownload(url, imageView))");
task = new BitmapDownloaderTask(imageView);
DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
imageView.setImageDrawable(downloadedDrawable);
imageView.setMinimumHeight(156);
task.execute(url);
}
}
/**
* Returns true if the current download has been canceled or if there was no download in
* progress on this image view.
* Returns false if the download in progress deals with the same url. The download is not
* stopped in that case.
*/
private static boolean cancelPotentialDownload(String url, ImageView imageView) {
Log.d(TAG, "---cancelPotentialDownload(String url, ImageView imageView)----)");
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
if (bitmapDownloaderTask != null) {
Log.d(TAG, "(bitmapDownloaderTask != null)");
String bitmapUrl = bitmapDownloaderTask.url;
if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
Log.d(TAG, "(((bitmapUrl == null) || (!bitmapUrl.equals(url)))");
bitmapDownloaderTask.cancel(true);
} else {
Log.d(TAG, "The same URL is already being downloaded.");
// The same URL is already being downloaded.
return false;
}
}
return true;
}
/**
* @param imageView Any imageView
* @return Retrieve the currently active download task (if any) associated with this imageView.
* null if there is no such task.
*/
private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
Log.d(TAG, "getBitmapDownloaderTask(ImageView imageView)");
if (imageView != null) {
Log.d(TAG, "(imageView != null) )");
Drawable drawable = imageView.getDrawable();
if (drawable instanceof DownloadedDrawable) {
Log.d(TAG, "(drawable instanceof DownloadedDrawable) ");
DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
return downloadedDrawable.getBitmapDownloaderTask();
}
}
return null;
}
Bitmap downloadBitmap(String stringUrl) {
Log.d(TAG, "(downloadBitmap(String stringUrl)");
URL url = null;
HttpURLConnection connection = null;
InputStream inputStream = null;
try {
url = new URL(stringUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setUseCaches(true);
inputStream = connection.getInputStream();
return BitmapFactory.decodeStream(new FlushedInputStream(inputStream));
/*
BitmapFactory.Options bfOptions=new BitmapFactory.Options();
bfOptions.inDither=false; //Disable Dithering mode
bfOptions.inPurgeable=true; //Tell to gc that whether it needs free memory, the Bitmap can be cleared
bfOptions.inInputShareable=true; //Which kind of reference will be used to recover the Bitmap data after being clear, when it will be used in the future
bfOptions.inTempStorage=new byte[32 * 1024];
Bitmap b=BitmapFactory.decodeStream(new FlushedInputStream(inputStream), null,bfOptions );
return b;
*/
} catch (IOException e) {
//getRequest.abort();
Log.w(TAG, "I/O error while retrieving bitmap from " + url, e);
} catch (IllegalStateException e) {
// getRequest.abort();
Log.w(TAG, "Incorrect URL: " + url);
} catch (Exception e) {
//getRequest.abort();
Log.w(TAG, "Error while retrieving bitmap from " + url, e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
return null;
}
/*
* An InputStream that skips the exact number of bytes provided, unless it reaches EOF.
*/
static class FlushedInputStream extends FilterInputStream {
public FlushedInputStream(InputStream inputStream) {
super(inputStream);
}
@Override
public long skip(long n) throws IOException {
long totalBytesSkipped = 0L;
while (totalBytesSkipped < n) {
long bytesSkipped = in.skip(n - totalBytesSkipped);
if (bytesSkipped == 0L) {
int b = read();
if (b < 0) {
break; // we reached EOF
} else {
bytesSkipped = 1; // we read one byte
}
}
totalBytesSkipped += bytesSkipped;
}
return totalBytesSkipped;
}
}
/**
* The actual AsyncTask that will asynchronously download the image.
*/
class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
private String url;
private final WeakReference<ImageView> imageViewReference;
public BitmapDownloaderTask(ImageView imageView) {
Log.w(TAG, "BitmapDownloaderTask(ImageView imageView)");
imageViewReference = new WeakReference<ImageView>(imageView);
}
/**
* Actual download method.
*/
@Override
protected Bitmap doInBackground(String... params) {
Log.w(TAG, "doInBackground");
url = params[0];
return downloadBitmap(url);
}
/**
* Once the image is downloaded, associates it to the imageView
*/
@Override
protected void onPostExecute(Bitmap bitmap) {
Log.w(TAG, "onPostExecute");
if (isCancelled()) {
bitmap = null;
}
addBitmapToCache(url, bitmap);
if (imageViewReference != null) {
Log.w(TAG, "(imageViewReference != null)");
ImageView imageView = imageViewReference.get();
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
// Change bitmap only if this process is still associated with it
// Or if we don't use any bitmap to task association (NO_DOWNLOADED_DRAWABLE mode)
if ((this == bitmapDownloaderTask)) {
Log.w(TAG, " if ((this == bitmapDownloaderTask)");
imageView.setImageBitmap(bitmap);
}
}
}
}
/**
* A fake Drawable that will be attached to the imageView while the download is in progress.
*
* <p>Contains a reference to the actual download task, so that a download task can be stopped
* if a new binding is required, and makes sure that only the last started download process can
* bind its result, independently of the download finish order.</p>
*/
static class DownloadedDrawable extends ColorDrawable {
private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;
public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
super(Color.BLACK);
Log.w(TAG, "DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) ");
bitmapDownloaderTaskReference = new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
}
public BitmapDownloaderTask getBitmapDownloaderTask() {
Log.w(TAG, "BitmapDownloaderTask getBitmapDownloaderTask() ");
return bitmapDownloaderTaskReference.get();
}
}
/*
public void setMode(Mode mode) {
this.mode = mode;
clearCache();
}
*/
/*
* Cache-related fields and methods.
*
* We use a hard and a soft cache. A soft reference cache is too aggressively cleared by the
* Garbage Collector.
*/
// Hard cache, with a fixed maximum capacity and a life duration
private final HashMap<String, Bitmap> sHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 2, 0.75f, true)
// private final HashMap<String, Bitmap> sHardBitmapCache = new LinkedHashMap<String, Bitmap>()
{
// private static final long serialVersionUID = -695136752926135717L;
@Override
protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) {
Log.w(TAG, "removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) ");
Log.w(TAG, "size() : " + size());
Log.w(TAG, "HARD_CACHE_CAPACITY : " + HARD_CACHE_CAPACITY);
if (size() > HARD_CACHE_CAPACITY) {
Log.w(TAG, "(size() > HARD_CACHE_CAPACITY) ");
Log.e(TAG, "sSoftBitmapCache --> sSoftBitmapCache.put ");
// Entries push-out of hard reference cache are transferred to soft reference cache
sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
return true;
} else
return false;
}
};
//HARD_CACHE_CAPACITY / 2
// Soft cache for bitmaps kicked out of hard cache
// private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>();
private final Handler purgeHandler = new Handler();
private final Runnable purger = new Runnable() {
public void run() {
Log.e(TAG, "purger run");
// clearCache();
}
};
/**
* Adds this bitmap to the cache.
* @param bitmap The newly downloaded bitmap.
*/
private void addBitmapToCache(String url, Bitmap bitmap) {
Log.w(TAG, "--------addBitmapToCache-----------");
if (bitmap != null) {
Log.w(TAG, "(bitmap != null) ");
synchronized (sHardBitmapCache) {
//final Bitmap tryData = sHardBitmapCache.get(url);
// if (tryData != null) {
// sHardBitmapCache.remove(url);
// }
Log.w(TAG, " sHardBitmapCache.put(url, bitmap);");
sHardBitmapCache.put(url, bitmap);
}
}
}
/**
* @param url The URL of the image that will be retrieved from the cache.
* @return The cached bitmap or null if it was not found.
*/
private Bitmap getBitmapFromCache(String url) {
Log.e(TAG, " --------getBitmapFromCache(String url)----------------");
// First try the hard reference cache
synchronized (sHardBitmapCache) {
final Bitmap bitmap = sHardBitmapCache.get(url);
if (bitmap != null) {
Log.e(TAG, " getBitmapFromCache ---> Bitmap found in Hard cache!!!!!");
// Bitmap found in hard cache
// Move element to first position, so that it is removed last
sHardBitmapCache.remove(url);
sHardBitmapCache.put(url, bitmap);
return bitmap;
}else{
Log.e(TAG, " getBitmapFromCache ---> Bitmap not found in Hard cache");
}
}
// Then try the soft reference cache
SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(url);
if (bitmapReference != null) {
Log.e(TAG, " getBitmapFromCache ---> bitmapReference found in SoftReference cache!");
final Bitmap bitmap = bitmapReference.get();
if (bitmap != null) {
Log.e(TAG, "Bitmap found in SoftReference cache!!!!!!!");
// Bitmap found in soft cache
return bitmap;
} else {
Log.e(TAG, "oooooooooooooooooooo --> Soft reference has been Garbage Collected");
// Soft reference has been Garbage Collected
sSoftBitmapCache.remove(url);
}
}
return null;
}
/**
* Clears the image cache used internally to improve performance. Note that for memory
* efficiency reasons, the cache will automatically be cleared after a certain inactivity delay.
*/
public void clearCache() {
Log.e(TAG, "----------------clearCache() ------------------");
synchronized (sHardBitmapCache) {
//sHardBitmapCache.clear();
}
//sSoftBitmapCache.clear();
sHardBitmapCache.clear();
sSoftBitmapCache.clear();
}
/**
* Allow a new delay before the automatic cache clear is done.
*/
private void resetPurgeTimer() {
purgeHandler.removeCallbacks(purger);
purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE);
}
}
I completely understand the logic behind this example however I am confused why this does not work properly.
We have two levels of cache :The Hard cache and the SoftReference cache. We save the bitmaps in the hard cache and then when is filled we save in the SoftReference and reorder hard cache..
Then I close the app, close the internet and restart it.In the constructor we call resetPurgeTimer() which clears the cache and therefore nothing is in the cache. To avoid this I commented that line in order not to clear the cache.. I restart my app and I can see 6-7 images which are present in SoftReference cache (according to LogCat)..
I know that SoftReference cache maybe get empty when GC is called but it is almost empty even before closing the app or disabling wifi...There is a well known bug with SoftReferences which are garbage collected even without memory getting low..
Is something wrong with this widely used example?
Thanks in advance,
Andreas
Yes, in Android 3.0 and above you can't use Weak or Soft References for Bitmaps. The JVM will aggressively collect them. Instead you should use an LRUCache (in the support library) to cache them. I'd also recommend setting the size of the max cache to some fraction of the size of the max heap (like 1/6 for example).