Loading Images Over HTTP on a Separate Thread on Android

My previous post about making a list view with lazy-loaded images on Android just had the code pertaining to the list view. As requested, I’m also adding the class that loads the images.

This class includes a queue so that only a single image is loaded at a time. I chose this because in building a list view with images it’s more important to start loading some images than to wait for all images to get loaded. Other implementations of this kind of thing launch a separate thread per image which means that the network connection would be clogged with all the image loads.

You’ll notice the use of SoftReference here, which I gleaned from Tom van Zummeren’s tutorial. While this appears to work well, I haven’t done any significant load or performance testing, so it may not be necessary.

There are some notable problems here in the design, so please adapt this to your need. Beyond the potential race condition noted below, there’s a basic problem in that the thread completes once the queue is done. So if the images happened to load faster than you add them to the queue you could end up with a queue that was emptied, the thread died and future items were never loaded. I’ve tried to work around this by capturing the TERMINATED state of the thread and relaunching it, but have not, as far as I know, tested this in production. And I’ve built no automated tests to test that case yet.

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.Thread.State;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.util.Log;

/**
 * This is an object that can load images from a URL on a thread.
 *
 * @author Jeremy Wadsack
 */
public class ImageThreadLoader {
	private static final String TAG = "ImageThreadLoader";

	// Global cache of images.
	// Using SoftReference to allow garbage collector to clean cache if needed
	private final HashMap<String, SoftReference<Bitmap>> Cache = new HashMap<String,  SoftReference<Bitmap>>();

	private final class QueueItem {
		public URL url;
		public ImageLoadedListener listener;
	}
	private final ArrayList<QueueItem> Queue = new ArrayList<QueueItem>();

	private final Handler handler = new Handler();	// Assumes that this is started from the main (UI) thread
	private Thread thread;
	private QueueRunner runner = new QueueRunner();;

	/** Creates a new instance of the ImageThreadLoader */
	public ImageThreadLoader() {
		thread = new Thread(runner);
	}

	/**
	 * Defines an interface for a callback that will handle
	 * responses from the thread loader when an image is done
	 * being loaded.
	 */
	public interface ImageLoadedListener {
		public void imageLoaded(Bitmap imageBitmap );
	}

	/**
	 * Provides a Runnable class to handle loading
	 * the image from the URL and settings the
	 * ImageView on the UI thread.
	 */
	private class QueueRunner implements Runnable {
		public void run() {
			synchronized(this) {
				while(Queue.size() > 0) {
					final QueueItem item = Queue.remove(0);

					// If in the cache, return that copy and be done
					if( Cache.containsKey(item.url.toString()) && Cache.get(item.url.toString()) != null) {
						// Use a handler to get back onto the UI thread for the update
						handler.post(new Runnable() {
							public void run() {
								if( item.listener != null ) {
									// NB: There's a potential race condition here where the cache item could get
									//     garbage collected between when we post the runnable and it's executed.
									//     Ideally we would re-run the network load or something.
									SoftReference<Bitmap> ref = Cache.get(item.url.toString());
									if( ref != null ) {
										item.listener.imageLoaded(ref.get());
									}
								}
							}
						});
					} else {
						final Bitmap bmp = readBitmapFromNetwork(item.url);
						if( bmp != null ) {
							Cache.put(item.url.toString(), new SoftReference<Bitmap>(bmp));

							// Use a handler to get back onto the UI thread for the update
							handler.post(new Runnable() {
								public void run() {
									if( item.listener != null ) {
										item.listener.imageLoaded(bmp);
									}
								}
							});
						}

					}

				}
			}
		}
	}

	/**
	 * Queues up a URI to load an image from for a given image view.
	 *
	 * @param uri	The URI source of the image
	 * @param callback	The listener class to call when the image is loaded
	 * @throws MalformedURLException If the provided uri cannot be parsed
	 * @return A Bitmap image if the image is in the cache, else null.
	 */
	public Bitmap loadImage( final String uri, final ImageLoadedListener listener) throws MalformedURLException {
		// If it's in the cache, just get it and quit it
		if( Cache.containsKey(uri)) {
			SoftReference<Bitmap> ref = Cache.get(uri);
			if( ref != null ) {
				return ref.get();
			}
		}

		QueueItem item = new QueueItem();
		item.url = new URL(uri);
		item.listener = listener;
		Queue.add(item);

		// start the thread if needed
		if( thread.getState() == State.NEW) {
			thread.start();
		} else if( thread.getState() == State.TERMINATED) {
			thread = new Thread(runner);
			thread.start();
		}
		return null;
	}

	/**
	 * Convenience method to retrieve a bitmap image from
	 * a URL over the network. The built-in methods do
	 * not seem to work, as they return a FileNotFound
	 * exception.
	 *
	 * Note that this does not perform any threading --
	 * it blocks the call while retrieving the data.
	 *
	 * @param url The URL to read the bitmap from.
	 * @return A Bitmap image or null if an error occurs.
	 */
	public static Bitmap readBitmapFromNetwork( URL url ) {
		InputStream is = null;
		BufferedInputStream bis = null;
		Bitmap bmp = null;
		try {
			URLConnection conn = url.openConnection();
			conn.connect();
			is = conn.getInputStream();
			bis = new BufferedInputStream(is);
			bmp = BitmapFactory.decodeStream(bis);
		} catch (MalformedURLException e) {
			Log.e(TAG, "Bad ad URL", e);
		} catch (IOException e) {
			Log.e(TAG, "Could not get remote ad image", e);
		} finally {
			try {
				if( is != null )
					is.close();
				if( bis != null )
					bis.close();
			} catch (IOException e) {
				Log.w(TAG, "Error closing stream.");
			}
		}
		return bmp;
	}

}
About these ads

34 thoughts on “Loading Images Over HTTP on a Separate Thread on Android

  1. Hi,

    Thanks for your code. I implemented lazy loading using Tom van Zummeren’s and your tutorial and it works fine for me. Now I am trying to have a progress bar displayed in the listview until images are loaded. However I am facing a problem. My progress bar get displayed well for all the rows and when the images are loaded only the first row in the list view displays properly, in the sense only the progress bar from the first row disappears when the image is loaded but the rest of the progress bars continue to be displayed even if the images are loaded. Have you worked with any such thing. Please can you let me know if you have an idea on this.

    Thanks,
    Piya.

  2. Piya –

    I haven’t tried to implement something like that. We just used a static placeholder image. The images load fast enough that there really isn’t time to show a progress indicator (for our implementations). I would also be concerned about the performance hit of many progress indicators running (on G1 and other early devices). Also consider user-experience: Are you distracting the eye from the important information with animations?

      • The way I’ve seen this done is by using a WebView instead of the thread code I suggest. It seemed heavy handed to me but that could be one approach.

        Using this code you could do it by just setting all images for the image views to the indeterminate progress bar graphic in the layout and then replace the image in the image view in the thread as is done now.

  3. Thanks for your precious tutorial. I need to load the image from the sdcard instead the net, so i modified the code in this way: http://pastebin.com/T8reGkGC
    Calling in a way that is exaplained in your previous post it works but sometimes images are placed in the wrong row… Any idea?

  4. Rciovati – It looks like your pastebin was removed, so I can’t see your code. Anyway, if you are loading from the SD card, you don’t really need a thread, do you? You should be able to load the image directly just using the mapping in your adapter. I think that would most effectively fix your issue.

    To your question however, the images loading in the wrong row is a side-effect of Android’s reuse of the view object when drawing items in the ListView. It does this to save memory and improve performance. If you override getView in a custom adapter as I showed in my previous post, then not checking the convertView should address this.

  5. Thanks for your reply Jeremy
    Code is here: http://pastebin.com/J9vktnFB, if can be usefull to someone.
    However, i use convertvVew in order to benefit of the reuse of the view object, i don’t know if can be more efficient renounce at that or load images in a thread…

  6. Well, your pastebin shows me how to get web content cleanly using Android logic. So it’s useful to me. Why Android team has to come up with a different and non-discoverable way of loading remote content for a language that has had that feature built into its design is beyond me. And a topic for a different rant.

    As I understand it, you can either use convertView for efficiency or you can create a new view that can persist beyond the scope of the method. You can’t do both because of the reuse. So you’ll need to decide whether performance is better using convertView and not threading or using threads and not using convertView.

  7. droid –

    Android only draws views for visible cells in a list view. When it first loads, the initial views should all be loaded through your adapter and your adapter should call notifyDatasetChanged on to let the UI control update it’s display, as I have done at http://ballardhack.wordpress.com/2010/04/05/loading-remote-images-in-a-listview-on-android/

    If you are not getting updated image loads during scrolling, then I suspect that your adapter isn’t getting called for newly created cell views. I had to inflate the view for each cell rather than use a cached view to get this to work right. While this means more overhead and memory use, it worked fine for smaller lists.

  8. Jeremy, I come back to this with an update: I found out that queue was fill up with many requests downloading again and again the same images (so when scrolling new items where in the very bottom of the queue). I saw that checking in the cache was failing, due to comparing a URI String with a URL object. So I modified the code with item.url.toString() and now it works.

    One more question. When the SoftReference clears some items in the Cache, I get an empty image in my list. Any ideas on how could I refresh and download again the image?
    I tried onResume() but no success.
    Thanks for the help.

  9. Good catch. Line 65 in the code should use item.url.toString() as is used below (when added at lin 83). I’ve updated the code on this page to reflect that change.

    I’m not sure about the SoftReference problem — if you’re using loadImage, above (or something like it), any item cleared from the cache should should return null for a cleared reference and add it to the queue. You might look at the code in lines 124-130: I never tested restarting the thread (that I know) and there may be a better way to do that (like other thread states to check for).

  10. > there’s a basic problem in that the thread completes once the queue is done.

    I think you can get around that (and also simplify the code) by using a ThreadPoolExecutor with a minimum of no threads and a maximum of one thread. This way, it will still accept tasks after the thread has been spun down (it will just spawn a new one).

    It is good form to explicitly shutdown the Executor once no longer needed (which would put us in the same predicament again), but if the single thread it uses gets idle and terminated, it does not seem to cause any problem if you don’t.

    I am using it here[1] and it seems to work.

    [1] https://github.com/thiloplanz/twitter-android-sdk/commit/c0f149b073bafd507516d3052eb43b8c3aa80fa2#diff-0

    • Thilo –

      Good suggestion and thanks for the link to the implementation. I have done additional work on this for a current project and will be posting a more revised version of this in the next couple weeks. Suffice to say that under my current use the Thread restart appears to be working perfectly, but as I’m not an expert in multi-threaded Java I’m going to look into your ThreadPoolExecutor to increase robustness.

      • Droid,
        did you find a solution for that SoftReference problem?
        I think I am facing the same problem…

        Jeremy, do you think you will be able to post your more revised version soon?

        Thanks a lot guys!

  11. vincgros – As it happens I just got to posting the updated code today. See the ImageThreadLoader from the NPR News app source.

    That has significant changes, including on-disk caching, which is what you want to do for anything other than very small bitmaps. It checks for null on the cached item (line 116 in the linked file) so that handles when SoftReference drops the bitmap.

    Finally, not in the implementation of this (starting at line 161 in the NewsListAdapter), that I’m not finding the view by index/position, rather than holding a reference to a view that may be cached or reused. This may resolve the issue of missing images.

    • Dear Jeremy, thanks for this insight and the code links. Understanding it helps me a lot.
      Just btw. I had problems trying the NPR ImageThreadLoader. Eclipse tells me that ImageLoadedListener could not be instantiated when defining Drawable cachedImage. Have you come across this issue?

      • Stefan, You can’t instantiate ImageThreadLoader directly — it’s an interface. You have to subclass it, but you can create an anonymous subclass as I did in the implementation in the previous post linked at the top of the article.

      • Oh boy, looks like I mixed up ImageLoadedListener (interface) and ImageLoadListener (the interfaces implementation). Tricky thing with those similar names…

  12. I solve that porblem when softrefrence type collected by GC, null Bitmap returned so Blank Image apear. first i have changed line 65: if( Cache.containsKey(item.url.toString()) && Cache.get(item.url.toString()) != null) –> if( Cache.containsKey(item.url.toString()) && Cache.get(item.url.toString()).get() != null)

    and ‘loadImage’ function something Changed.
    When Cache Contains Uri Key And Return Bitmap is null I dont’t know the state whether Loading not completed , Collected So there’s need to Check
    my English is poor . Now I will show all of my modified source

    import java.io.BufferedInputStream;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.lang.Thread.State;
    import java.lang.ref.SoftReference;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLConnection;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.Iterator;
    
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.graphics.Bitmap.CompressFormat;
    import android.graphics.drawable.BitmapDrawable;
    import android.graphics.drawable.Drawable;
    import android.os.Handler;
    import android.util.Log;
    
    public class ImageThreadLoader {
    	
    	private CompressFormat compressedImageFormat = CompressFormat.PNG;
    	private int cachedImageQuality = 75;
    	private final HashMap&lt;String, SoftReference&gt; Cache = new HashMap&lt;String,  SoftReference&gt;();
    	
    	private final class QueueItem {
    		public URL url;
    		public ImageLoadedListener listener;
    	}
    	private final ArrayList Queue = new ArrayList();
    	private final ArrayListQueueKey = new ArrayList();
    	private String CurLoadKey ="";
    
    	private final Handler handler = new Handler();	// Assumes that this is started from the main (UI) thread
    	private Thread thread;
    	private QueueRunner runner = new QueueRunner();;	
    	
    	/** Creates a new instance of the ImageThreadLoader */
    	public ImageThreadLoader() {
    			thread = new Thread(runner);
    	}
    	
    		
    	public interface ImageLoadedListener {
    		public void imageLoaded(Bitmap imageBitmap );
    	}
    	
    	private class QueueRunner implements Runnable {
    		public void run() {
    			synchronized(this) {
    				while(Queue.size() &gt; 0) {
    					final QueueItem item = Queue.remove(0);
    					CurLoadKey = QueueKey.remove(0);
    					
    					// If in the cache, return that copy and be done
    					if( Cache.containsKey(item.url.toString()) &amp;&amp; Cache.get(item.url.toString()).get() != null) {
    						// Use a handler to get back onto the UI thread for the update
    						
    						handler.post(new Runnable() {
    							public void run() {
    								if( item.listener != null ) {
    									// NB: There's a potential race condition here where the cache item could get
    									//     garbage collected between when we post the runnable and it's executed.
    									//     Ideally we would re-run the network load or something.
    									SoftReference ref = Cache.get(item.url.toString());
    									if( ref != null ) {
    										item.listener.imageLoaded(ref.get());
    									}
    								}
    							}
    						});
    					} else {
    						
    						final Bitmap bmp = readBitmapFromNetwork(item.url);
    						
    						if( bmp != null ) {
    							Cache.put(item.url.toString(), new SoftReference(bmp));
    														
    							// Use a handler to get back onto the UI thread for the update
    							handler.post(new Runnable() {
    								public void run() {
    									if( item.listener != null ) {
    										item.listener.imageLoaded(bmp);
    									}
    								}
    							});
    							
    						}
    					}
    
    				}
    			}
    		}
    	}
    	
    	public Bitmap loadImage( final String uri, final ImageLoadedListener listener) throws MalformedURLException {
    		// If it's in the cache, just get it and quit it
    		Log.e("*********", "--1");
    		if( Cache.containsKey(uri)) {
    			
    			SoftReference ref = Cache.get(uri);
    			if( ref != null ) {
    				Bitmap tmpbitmap =ref.get();
    				if(tmpbitmap == null)
    				{
    					if(!QueueKey.contains(uri) &amp;&amp;  !CurLoadKey.equals( uri))
    					{
    		//				Cache.remove(uri);
    												
    						QueueItem item = new QueueItem();
    						item.url = new URL(uri);
    						item.listener = listener;
    						QueueKey.add(uri);						
    						Queue.add(item);
    						
    						// start the thread if needed
    						if( thread.getState() == State.NEW) {
    							thread.start();
    						} else if( thread.getState() == State.TERMINATED) {
    							thread = new Thread(runner);
    							thread.start();
    						}						
    					}
    					return tmpbitmap;
    						
    				}
    				else				
    					return tmpbitmap;
    			}			
    		}
    		
            Log.e("*********", "--3");
    		QueueItem item = new QueueItem();
    		item.url = new URL(uri);
    		item.listener = listener;
    		QueueKey.add(uri);		
    		Queue.add(item);
    
    		
    		// start the thread if needed
    		if( thread.getState() == State.NEW) {
    			thread.start();
    		} else if( thread.getState() == State.TERMINATED) {
    			thread = new Thread(runner);
    			thread.start();
    		}
    		
    		return null;
    	}	
    
    	public static Bitmap readBitmapFromNetwork( URL url ) {
    		InputStream is = null;
    		BufferedInputStream bis = null;
    		Bitmap bmp = null;
    		Log.e("*********", "--4");
    		try {
    			URLConnection conn = url.openConnection();
    			conn.connect();
    			is = conn.getInputStream();
    			bis = new BufferedInputStream(is);
    			bmp = BitmapFactory.decodeStream(bis);
    		} catch (MalformedURLException e) {
    			Log.e("*************", "Bad ad URL", e);
    		} catch (IOException e) {
    			Log.e("******************", "Could not get remote ad image", e);
    		} finally {
    			try {
    				if( is != null )
    					is.close();
    				if( bis != null )
    					bis.close();
    			} catch (IOException e) {
    				Log.w("*************", "Error closing stream.");
    			}
    		}
    		Log.e("*********", "--5");
    		return bmp;
    	}
    	
    	public void clear() {
    		// TODO Auto-generated method stub
    	/*	
    		for(SoftReference bitmap :Cache.values() )
    			bitmap=null;
    		Cache.clear();
    		
    		*/
    	}
    	
    	public void SetImage(String imgUrl, Bitmap bitmap) {
    		// TODO Auto-generated method stub
    		if(Cache.containsKey(imgUrl))
    		{				
    			Cache.remove(imgUrl);
    			Cache.put(imgUrl,new SoftReference( bitmap));		
    		}
    	}
    	public void RemoveIamgeExcept(ArrayList liveImage) {
    		// TODO Auto-generated method stub
    		/*
    		for ( Iterator iter = Cache.keySet().iterator(); iter.hasNext(); )
            {
            String item = iter.next();
            if ( !liveImage.contains(item))
                {
                // remove from any state with a space in its long name.
                iter.remove();// avoids ConcurrentModificationException
                }
            }
            */
    
    	/*	for ( String key : Cache.keySet() )
            {
    			if(!liveImage.contains(key))
    				Cache.remove(key);
            }	
    */
    	}
    }
    
  13. Hi everyone,Jeremy thank for your tutorial it’s very useful for me,and also thank to Jeffry MyeongJin Kim whit him code now it work fine,but I have a problem,i take links of image calling web services.some object that I receive haven’t an url’s image.Using the code if an object haven’t url,it load the precedent image,how can i manage this problem?

  14. Hi Jeffry,

    I’m using ur code but i’m getting errors at final QueueItem item = Queue.remove(0);
    CurLoadKey = QueueKey.remove(0);.. and
    item.listener.imageLoaded(ref.get());.

    kindly let me know the problem.

    Thanks
    Sai

  15. Hi nice tutorial!I have to do kind of the same thing.The only diference is that instead of streaming from a URL i get the streamed data from a wcf service.Do you know how i get to use the web service instead?

    • What’s the response from the WCF service? Is it SOAP? There are lots of SOAP libraries for JAVA. JSON? Also true. Are you rendering an image from the WCF response or building objects? If the latter I’d suggest creating a service to handle brokering rather than anything like this.

      • Oh i just saw your reply :) yes the response is in soap and i’m using ksoap2 to parse it.The thing is that now the situation is a bit different :/ getting all the bulk at once doesn’t seem to work,so alternatively i need to stream from the wcf.Can you point me at any direction ’cause i feel kinda lost i’ve been searching for days and i can’t seem to find out how I’m supposed to stream the data from internet.Was that ‘brokening’ that you suggested a stream-like procedure?also don’t care about the wcf now it is configured to buffered but it is up to me now to change it so it’s of no importance.Thx so much for replying me before

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s