Last-in/First-Out Queue for android asynchronous tasks that require background processing and updates on UI when completed

23Jan12

During the development of the  “Photo Voyages of Trey Ratcliff” app, I ran into a problem with file IO read operations.  Bascially, the read of a large photo (300KB-900KB) would take from 300 ms to 2000 ms based on file system load. Because of this, I needed to do the read in the background so that it wouldn’t affect the UI’s responsiveness.

That was trivial to do, but I then ran into problems when the user was swiping through lots of photos in a short periode of time. The root cause was that the read operations were not able to finish before the next on came, and soon I could have 15-20 ongoing file reads. This would also cause memory problems.

In order to get around this, I’ve implemented a generic last-in/first-out queue for asynchronous tasks that require background processing and updates on UI when completed. All my file reads are queued through this and it has had a big effect on performance. Especially, since it is using WeakReferences to make sure the object can be garbage collected and checks if it is valid before the photo is read.

My code consist of three classes:

  • AsyncQueuableObject: The interface to implement for each task
  • AsyncReadQueue : The last-in/first-out queue that takes AsyncQueuableObjects
  • QueueablePhotoObject: Implementation of the interface for my use

AsyncQueuableObject


package com.elsewhat.slideshow.api;

public interface AsyncQueueableObject {
/**
*
* Perform the operation in a background thread
* @return
*/
public void performOperation();

/**
* Handle the result in the UI thread
*
* @param result
*/
public void handleOperationResult();
}

AsyncReadQueue


package com.elsewhat.slideshow.api;
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.Iterator;
import java.util.Stack;

import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;

/**
* Class which implements a Last-In/First-Out queue for operations that require background processing and an update to the UI thread
*
* My usage is to read large photos from the file system
*
* @author dagfinn.parnas
*/
public class AsyncReadQueue {
protected static final String LOG_PREFIX="Slideshow PhotoIOQueue";
//if the tasks can be processed in parallel (for example http), increase this to 2 or more
protected int numberOfThreads=1;
//that workers
protected ArrayList readerTasks;

Context context;
//stack is synchronized
Stack queuedObjects;
AsyncQueueListener listener;

/**
* Constructor
* @param context
* @param listener The listener which will be notified when a task has completed
*/
public AsyncReadQueue(Context context,AsyncQueueListener listener) {
super();
this.context = context;
this.listener=listener;

//lets make sure the List is synchronized
queuedObjects= new Stack();
}

/**
* Listener which is notified when the read is finished is complete or retrieved error
*/
public interface AsyncQueueListener {
/*These methods should be synchronized when implemented*/
void onAsyncReadComplete (AsyncQueueableObject queueableObject);
}

/**
* Add an object to the queue.
* Will trigger the worker tasks to start if they are not running
*
* @param queueObject
*/
public void add(AsyncQueueableObject queueObject){
queuedObjects.push(queueObject);

if(!hasRunningTasks()){
if(queuedObjects.size()>1){
Log.d(LOG_PREFIX, "Added new queued object, queue size="+queuedObjects.size()+". New tasks triggered"+ queueObject );
}

processQueue();
}else {
Log.d(LOG_PREFIX, "Added new queued object, queue size="+queuedObjects.size()+". Processed by already running tasks "+ queueObject );
}

}

protected void processQueue(){
//if this is called, we assume there hasRunningTasks()==false has been called first
//Log.i(LOG_PREFIX, "Starting new reader tasks to process queue" );
readerTasks= new ArrayList(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
ReaderTask readerTask = new ReaderTask(i);
readerTasks.add(readerTask);
readerTask.execute();
}
}

protected boolean hasRunningTasks(){
if(readerTasks==null || readerTasks.size()==0){
return false;
}
boolean hasActiveTask= false;
for (Iterator iterator = readerTasks.iterator(); iterator.hasNext();) {
ReaderTask readerTask = iterator.next();
if(readerTask.isFinished()==false){
hasActiveTask=true;
}
}
if(hasActiveTask){
return true;
}else {
return false;
}
}

/**
* Stops the ongoing tasks. Any queued objects are removed.
* To start it again, a new call to add method must be made
*
*/
public void stop(){
for (Iterator iterator = readerTasks.iterator(); iterator.hasNext();) {
ReaderTask readerTask = iterator.next();
if(readerTask.isFinished()==false){
Log.i(LOG_PREFIX, "Stopping ongoing async reader task");
readerTask.cancel(true);
}
}
queuedObjects.clear();

}

/**
* AsyncTask which represent the worker threads
*
*/
public class ReaderTask extends AsyncTask {
boolean hasError=false;

Throwable throwable;
String userErrorMsg;
int threadId;
boolean isFinished=false;
T result;

public ReaderTask(int threadId){
this.threadId=threadId;
isFinished=false;
}

@Override
protected Void doInBackground(Void... arg0) {
//loop which continues until the queue/stack is empty
while(!queuedObjects.isEmpty()){
if(isCancelled()){
Log.d(LOG_PREFIX,"Async task was cancelled");
return null;
}
AsyncQueueableObject asyncObject=null;
try {
asyncObject= queuedObjects.pop();
}catch (EmptyStackException e) {
//can happen if we have more than one running ReaderTask
return null;
}
if(asyncObject!=null){
//process the async operation
//this is the time consuming task
asyncObject.performOperation();

//publish progress will cause the handleOperationResult to be run on the UI thread
publishProgress(asyncObject);
}

}
isFinished=true;
return null;
}

/**
* Called when one of the queue objects have performmed the operation
*
* @see android.os.AsyncTask#onProgressUpdate(Progress[])
*/
@Override
protected void onProgressUpdate(AsyncQueueableObject... asyncObjects) {
if(asyncObjects.length==1){
AsyncQueueableObject asyncObject = asyncObjects[0];
asyncObject.handleOperationResult();
listener.onAsyncReadComplete(asyncObject);
}else {
Log.w(LOG_PREFIX, "Unexpected number of DownloadableObject in onProgressUpdate:"+asyncObjects.length );
}
}

protected boolean isFinished(){
return isFinished;
}

@Override
protected void onPostExecute(Void result) {
//isFinished=true;
//Log.i(LOG_PREFIX, "Async reader task " + threadId + " completed" );

//handle a case where a new queued object has been added at the same time we are finished this task
if(queuedObjects!=null && queuedObjects.size()>0 && hasRunningTasks()==false){
//Log.w(LOG_PREFIX, "Queue not empty at end of run. Restarting async reader tasks" );
processQueue();
}
}

/* (non-Javadoc)
* @see android.os.AsyncTask#onCancelled()
*/
@Override
protected void onCancelled() {
Log.i(LOG_PREFIX, "Download task stopped");
super.onCancelled();
}
}
}

QueueablePhotoObject


package com.elsewhat.slideshow.api;

import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;

import com.stuckincustoms.slideshow.premium.R;

import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;

/**
* Queueable object that cause a heavy file IO read load (300-2000 ms)
*
* Will use WeakReferences in order to make sure objects are still needed when it is ready for processing
*
* @author dagfinn.parnas
*
*/
public class QueueablePhotoObject implements AsyncQueueableObject {
protected SlideshowPhoto slideshowPhoto;
protected File rootFolder;
protected WeakReference weakRefslideshowView;
protected ImageView myImageView;
protected boolean wasGarbageCollected = false;
protected boolean wasOutOfMemory = false;
protected Drawable result;
protected String tagOnCompletion=null;
protected int maxWidth;
protected int maxHeight;

/**
* Constructor The View is stored as a WeakReference
*
* @param slideshowPhoto
* @param imageView
*/
public QueueablePhotoObject(SlideshowPhoto slideshowPhoto,
View slideshowView, File rootFolder, String tagOnCompletion, int maxWidth, int maxHeight) {
this.slideshowPhoto = slideshowPhoto;
this.rootFolder = rootFolder;
this.weakRefslideshowView = new WeakReference(slideshowView);
this.tagOnCompletion=tagOnCompletion;
this.maxWidth=maxWidth;
this.maxHeight=maxHeight;
}

/**
* Perform the operation of reading the photo from file in the background
*
*/
@Override
public void performOperation() {
View slideshowView = weakRefslideshowView.get();
if (slideshowView == null) {
wasGarbageCollected = true;
return;
} else {
try {
result= slideshowPhoto.getLargePhotoDrawable(rootFolder,maxWidth,maxHeight);
return;
} catch (OutOfMemoryError e) {
Log.i("QueueablePhotoObject",
"Out of memory while getting drawable");
wasOutOfMemory = true;
return;
} catch (IOException e2) {
Log.i("QueueablePhotoObject",
"IOException file reading photo "+ slideshowPhoto,e2);
wasOutOfMemory = true;
}
}
}

/**
* Handle operation results will be run on the UI thread
* and will be responsible for setting the read drawable to the ImageView drawable
*
*/
@Override
public void handleOperationResult() {
View slideshowView = weakRefslideshowView.get();

if(wasOutOfMemory){
return;
}else if(wasGarbageCollected){
return;
}else if (slideshowView == null) {
Log
.d("QueueablePhotoObject",
"Drawable loaded, but imageview has been garbage collected since read started");
return;
} else {
ImageView imageView = (ImageView)slideshowView.findViewById(R.id.slideshow_photo);
imageView.setImageDrawable(result);
if(tagOnCompletion!=null){
imageView.setTag(tagOnCompletion);
}
imageView.requestLayout();
return;
}
}

public String toString(){
if(slideshowPhoto!=null){
return "QueueablePhotoObject:"+slideshowPhoto.getTitle();
}else {
return super.toString();
}
}
}

About these ads


4 Responses to “Last-in/First-Out Queue for android asynchronous tasks that require background processing and updates on UI when completed”

  1. 1 Pooja

    Is there a license file for the code hosted at https://github.com/elsewhat/com.elsewhat.android.slideshow ?

    • Thanks for the interest.

      I’ll add a license shortly.

      The code is available for commercial and non-commercial uses, but I’d like a small attribution/mention (so that it might spread more) . Based on the above I’ll use an Apache 2.0 license.

      If this is not suitable for you, let me know and I’m sure we can arrange something

      • 3 Pooja

        Do any of these work ?

        1.Apache 1.1
        2.Boost Software License 1.0
        3.BSD-type
        4.ISC License
        5.MIT-type
        6.New BSD-type
        7.Open SSL
        8.Public Domain
        9.W3C
        10.Zlib License

      • I’ll add an MIT or BSD type license to the project in the next few days


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 )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: