Is BitmapFactory.decodeStream Thread safe? Sometimes my Async task finishes before it completes

2.5k Views Asked by At

I've got the following Async task, which should just load up an image from a given URL. The images do exist and I have access to them

private class FetchVehicleImage extends AsyncTask<String, Integer, Bitmap>
    {

        private ProgressBar mSpinner;
        private ImageView mImage;
        private String imagesBaseUrl = "http://mywebsite.net/images/";
        private URL url = null;

        @Override
        protected void onPreExecute()
        {
            mImage = (ImageView) findViewById(R.id.vehicle_image);
            mSpinner = (ProgressBar) findViewById(R.id.vehicle_image_progress_bar);
            mSpinner.setIndeterminate(true);
            mSpinner.setVisibility(View.VISIBLE);
            mImage.setVisibility(View.GONE);
        }

        @Override
        protected Bitmap doInBackground(String... strings)
        {
            Bitmap bm = null;

            try
            {
                url = new URL(imagesBaseUrl + strings[0]);

                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setDoInput(true);
                conn.connect();
                InputStream is = conn.getInputStream();
                bm = BitmapFactory.decodeStream(is);
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
            return bm;
        }

        protected void onPostExecute(final Bitmap result)
        {
            if (result != null)
            {
                mImage.setImageBitmap(result);
            }
            mImage.setVisibility(View.VISIBLE);
            mSpinner.setVisibility(View.GONE);
        }
    }

I never see an exception in the doInBackground, however sometimes bm is returned as null, but its very intermittent. I have 4 images, 3 of those load perfectly fine every time, but one only loads if I hit a breakpoint on the bm assignment, supposedly giving it enough time to do its work?

I thought that doInBackground should run on a background thread therefore I should always either get the image, or get an exception?

1

There are 1 best solutions below

0
On

N.B. If your app is merely running out of native backing memory for its bitmaps, then this approach will not help. If you are having hard-to-explain problems with bitmaps, especially pre-Honeycomb, I cannot overstate the importance of understanding the relationship of Dalvik heap to native backing memory. Mr. Dubroy's discussion of this was exceptionally helpful for me -- worth listening all the way through Dubroy's Heap Presentation

And, then my attempted answer to your question above . . . I can't prove it, but I have a very strong suspicion that it is not thread-safe. I hit this when I do image manipulation after fetching. As with your example above, when I request more than one image file, and process them as they arrive, I get OutOfMemory errors, which I catch, only to discover that both the heap and the available native backing memory are fine (>100k and >100M respectively). And, sometimes the fetch works (as you describe), but sometimes not. On some devices it is more robust than others. When called upon to invent a story for why this is, I imagine to myself that there may be image processing hardware (e.g. jpg encoders) on some devices and not others, which the native libraries of the OS may or may not avail themselves of. Then, I proceed immediately to blame those hardware bottlenecks for being not thread safe -- all without the smallest shred of anything resembling evidence. Anyway, the only approach that I found works on all the devices in my test stable (about a dozen) -- reliably -- is to isolate the bitmap manipulation parts and single-thread.

In your example above, you would still use the AsyncTask to actually fetch the files from the network, and to write them to storage someplace (the raw byte stream). When the AsyncTask completes (i.e. calls its delegate for onPostExecution), then you might do something like my Poster class below.

In my activity (where I make the multiple download requests), I create a global executor in the class which is instantiated initially in the UI-Thread:

public ExecutorService mImagePipelineTask = null;  // Thread to use for pipelining images (overlays, etc.)

And then initialize it:

        mImagePipelineTask = Executors.newSingleThreadExecutor();

Then, I dispense with using AsyncTask, to gain control of the number of threads in the Thread pool. My async bits look like this, instead:

   public class PosterImage extends HashMap<String, Object> {

        private final String TAG = "DEBUG -- " + ClassUtils.getShortClassName(this.getClass());
        private PosterImageDelegate mPosterDelegate = null;
        private Drawable mBusyDrawable = null;
        private Drawable mErrorDrawable = null;
        private ExecutorService mImagePipelineTask = null;

        /*
         * Globals
         */
        Context mContext = null;

        /*
         * Constructors
         */
        public PosterImage() {
        }

        public PosterImage(PlaygroundActivity aContext) {
            mContext = aContext;
            mImagePipelineTask = aContext.mImagePipelineTask; 
            mBusyDrawable = mContext.getResources().getDrawable(R.drawable.loading);
            mErrorDrawable = mContext.getResources().getDrawable(R.drawable.load_error);
        }

Then, some bits you probably don't care about . . . and then some initialization stuff, like how to set our delegate (you will want a PosterImageDelegate interface, of course):

    public void setPosterDelegate(PosterImageDelegate aPosterDelegate) {
        mPosterDelegate = aPosterDelegate;
    }

And then, the bits that do image manipulation, and as a side-effect, use the BitmapFactory (and Drawable) classes. To use this, you instantiate the PosterImage object, set yourself as the delegate, and then call this fellow:

    public Drawable getPreformattedFileAsync() {
        if(mFetchFileTask == null) {
            Log.e(TAG, " -- Task is Null!!, Need to start an executor");
            return(mErrorDrawable);
        }
        Runnable job = new Runnable() {
             public void run() {
                 Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
                 Thread.currentThread().yield();
                 if(mPosterDelegate != null) {
                     Drawable retDrawable = getPreformattedFile();
                     if(retDrawable != null) {
                            mPosterDelegate.onDrawableRequest(retDrawable);
                     }  else  {
                         mPosterDelegate.onDrawableRequest( mErrorDrawable);
                     }
                 }
             }
         };
         mImagePipelineTask.execute(job);
         return(mBusyDrawable);
    }

    public Drawable getPreformattedFile() {
        Drawable ret = null;
        try {
            FileInputStream in = new FileInputStream(preformattedFileName());
            ret = Drawable.createFromStream(in, null);
                    // do something interesting with the Drawable
        } catch( OutOfMemoryError e ) {
            System.gc();
            e.printStackTrace();
                        // Will return null on its own
        } catch( Exception e) {
            Log.e(TAG, "Trouble reading PNG file ["+e+"]");
        }
        return(ret);
    }

When this returns, the calling object (in the UI-Thread) has a 'busy' drawable. When the delegate gets called (after the file downloads and is converted to a Drawable by this thread, it is ready for loading into into whatever Drawable receiver you designate. Any number of images can be downloaded in parallel, and this guarantees that the background thread will only ever process one image at a time. Happily, it does not tie up the UI thread to do the image processing

(N.B. you still need a Handler in your calling class (the one that sets itself as the delegate) to have the UI thread actually put the Drawable into the receiving View/Layout/whatever). For attempted completeness, that might look like:

mHandler.post(new Runnable() {
    @Override
    public void run() {
        aItem.getButton().setBackgroundDrawable(aDrawable);
        aItem.getButton().postInvalidate();
}
});

Perhaps all this helps, perhaps not. But I would love to hear a definitive answer to the excellent question you pose.