Last-in/First-Out Queue for android asynchronous tasks that require background processing and updates on UI when completed
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();
}
}
}
Filed under: Android | Leave a Comment
Tags: android, gallery, outofmemoryexception, queue

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