1

I'm getting the following error in a crashlytics report and I'm having a little trouble figuring out where it's coming from or how to resolve it.

The reason I'm confused - is the crash seems to reference the Volley library it's-self, not any code I've actually written (nothing in the crash seems to point to any of my code).

Does anyone know how or what I might be able to do to resolve this crash?

Any suggestions are GREATLY appreciated.

Fatal Error:

Fatal Exception: java.util.ConcurrentModificationException
       at java.util.HashMap$HashIterator.nextEntry(HashMap.java:787)
       at java.util.HashMap$ValueIterator.next(HashMap.java:819)
       at com.android.volley.toolbox.ImageLoader$4.run(ImageLoader.java:464)
       at android.os.Handler.handleCallback(Handler.java:739)
       at android.os.Handler.dispatchMessage(Handler.java:95)
       at android.os.Looper.loop(Looper.java:148)
       at android.app.ActivityThread.main(ActivityThread.java:5527)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:730)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:620)

Source:

public class VolleyManager {
    private static final String TAG = VolleyManager.class.getSimpleName();

    /** Number of network request dispatcher threads to start. */
    private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

    private static VolleyManager mInstance;
    private RequestQueue mRequestQueue;
    private ImageLoader mImageLoader;
    private static Context mContext;

    private VolleyManager(Context context) {
        mContext = context.getApplicationContext();
        mRequestQueue = getRequestQueue();
    }

    public static synchronized VolleyManager getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new VolleyManager(context);
        }
        return mInstance;
    }

    @SuppressWarnings("deprecation")
    public RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {
            File cacheDir = new File(FMCacheManager.getCacheBaseDir(mContext));

            String userAgent = "volley/0";
            try {
                String packageName = mContext.getPackageName();
                PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0);
                userAgent = packageName + "/" + info.versionCode;
            } catch (NameNotFoundException ignore) {
            }

            HttpStack stack = new HurlStack();
            Network network = new BasicNetwork(stack);

            HandlerThread mHandlerThread = new HandlerThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND);
            mHandlerThread.start();

            ResponseDelivery delivery = new ExecutorDelivery(new Handler(mHandlerThread.getLooper()));
            RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network, DEFAULT_NETWORK_THREAD_POOL_SIZE, delivery);

            queue.start();

            mRequestQueue = queue;
        }
        return mRequestQueue;
    }


    public ImageLoader getImageLoader() {
        getRequestQueue();
        if (mImageLoader == null) {
            mImageLoader = new ImageLoader(this.mRequestQueue,
                    new LruBitmapCache());
        }
        return this.mImageLoader;
    }

    public <T> void addToRequestQueue(Request<T> req, String tag) {
        if (req != null) {
            Log.d(TAG, "request: " + req.getUrl());

            // set the default tag if tag is empty
            req.setTag(FMUtils.isNullOrEmpty(tag) ? TAG : tag);

            getRequestQueue().add(req);
        }
    }

    public void cancelPendingRequests(Object tag) {
        if (mRequestQueue != null) {
            mRequestQueue.cancelAll(tag);
        }
    }

}

1 Answers1

0

Yes, the stack trace suggests that the crash happens in ImageLoader's run() method and that it has to do with iterating over a HashMap.

You can see (some version of) ImageLoader.java in here. There's this for-each loop:

for (BatchedImageRequest bir : mBatchedResponses.values()) {
    for (ImageContainer container : bir.mContainers) {
        // If one of the callers in the batched request canceled the request
        // after the response was received but before it was delivered,
        // skip them.
        if (container.mListener == null) {
            continue;
        }
        if (bir.getError() == null) {
            container.mBitmap = bir.mResponseBitmap;
            container.mListener.onResponse(container, false);
        } else {
            container.mListener.onErrorResponse(bir.getError());
        }
    }
}

Now, if an another thread modifies the mBatchedResponses.values() or bir.mContainers while they are being iterated, a ConcurrentModificationException will occur. They are not "thread safe" collections.

There's this other recent discussion which points out that separate ImageLoader instances should be used for any simultaneous (parallel) image loading operations. Fernando Jascovich writes:

You have to implement two ImageLoaders, one for the thumbnails and another one for the full-res images.

So you could check if you start any parallel image download operations in your own code, have a look at that other discussion and follow the recommended approach.

As for a more generic answer to "How to diagnose ConcurrentModificationException": See the stack trace, look at the exact code file name and line number. If it's inside a for-each loop iterating over a collection you are probably modifying the collection in another thread at the same time. This might happen only occasionally and go unnoticed for a long time. The same exception also happens when modifying the collection inside the said for-each loop as explained e.g. in this discussion.

Community
  • 1
  • 1
Markus Kauppinen
  • 3,025
  • 4
  • 20
  • 30
  • I'm calling / creating ImageLoaders in two classes/places - my FMUtils class: http://pastebin.com/y3xkxSSz and my AdsController class: http://pastebin.com/0aHBz3WW BTW - I really appreciate your help with this - it's driving me crazy trying to fix this (I can't replicate it - but I keep getting crash reports about it) Also - the stacktrace, where should I be looking? com.android.volley.toolbox.ImageLoader ? (I'm just a little confused because the stacktrace doesn't seem to mention a line number of my code - instead it seems to point to the Volley library its self [I think]) – AndroidExterminator May 23 '16 at 18:40
  • Any ideas how I might resolve this? (I posted a few code snippets above) – AndroidExterminator May 23 '16 at 19:01
  • I'm not actually familiar with Volley so I can't comment on best practices etc. It's funny though that using a singleton "VolleyManager" like you have seems to be exactly what is recommended in tutorials, but then it may lead to a `ConcurrentModificationException`. I also can't really say if Fernando's advice is the "best practise" for using Volley, but having a look at the `ImageLoader.java` it does make sense. So you could try making your `VolleyManager` have two separate `ImageLoader` instances: one for `FMUtils` and one for `AdsController`. – Markus Kauppinen May 24 '16 at 07:44
  • It's anyway obvious that the exception occurs inside `ImageLoader.java` and it's the `mBatchedResponses` `HashMap` that's being iterated and modified concurrently. (As the crash log specifies a `HashMap` and `bir.mContainers` is a `LinkedList`.) It's iterated in [batchResponse()](https://android.googlesource.com/platform/frameworks/volley/+/master/src/main/java/com/android/volley/toolbox/ImageLoader.java#450) and modified in [cancelRequest()](https://android.googlesource.com/platform/frameworks/volley/+/master/src/main/java/com/android/volley/toolbox/ImageLoader.java#344). – Markus Kauppinen May 24 '16 at 07:52