bumptech / glide

An image loading and caching library for Android focused on smooth scrolling
https://bumptech.github.io/glide/
Other
34.67k stars 6.12k forks source link

Prefetching images best practices #2523

Closed romshiri closed 7 years ago

romshiri commented 7 years ago

Hi guys,

I'm using Glide and it works pretty well! I'm building an app which is kind of a gallery that shows the user all his albums and photos. I'm looking for a way to prefetch images before the user actually get into the gallery activity, so the scrolling would be smooth as possible and no "blank" images will be shown.

What I've done till now: As you probably guess, I'm using a recycle view with a GridLayoutManager, and when I bind a view holder, I use Glide to load the image. In order to make the scrolling smoother when user scroll down (on average speed), I made some tweaks to the LayoutManager so it would prefetch some view holders ahead of time. Doing so, also make a Glide request ahead of time. Good for us.

What's the problem then? The problem is when a user scrolls the list very fast, which is a reasonable case as the user would look for his old photos down in the list.

Then I thought that I should "warm-up" images ahead of time in the background, using Glide, before the user actually gets into the gallery activity

As suggested on the internet, I used the "downloadOnly/submit" API for this reason, though, it's not working well. It seems like when invoking this over all the photos in a gallery, it just makes the things worse when the user actually get into the gallery activity and the RecycleView try to load the images on each view holder binding. I guess Glide is overwhelmed with loading requests, so it couldn't handle it anymore.

for(Photo photo : photos) { FutureTarget<Bitmap> cachedImage = GlideApp.with(context).asBitmap() .load(photo.getPath()) .centerCrop() .submit(thumbnailSize, thumbnailSize); }

Any suggestions/best practices how to achieve a smooth scroll even when scrolling very fast?

sjudd commented 7 years ago

There are a bunch of things you can do. Here are a couple in rough order from most important to least:

  1. Get the images in the disk cache before your app is opened (JobScheduler can be useful for this). For local images what you're doing mostly makes sense. Use preload() to avoid having to run your own background thread. Use a lower priority to limit contention with UI requests. Clear the returned Targets when the user enters your Activity or at least when they start scrolling.

  2. Pick an appropriate thumbnail size, or maybe even multiple sizes. You can't do much to improve the initial decode time for local images since it's a function of the size of image captured on the device and the sdcard/cpu speed of the device. You can do a lot to improve the rate at which you can pull thumbnails out of the disk cache, mostly by changing the size of the images you store.

  3. Use Glide's RecyclerView integration library to preload images a few rows ahead of where the user is scrolling.

Also verify that you're actually using your disk cache, especially if you're preloading images. All options, including the transformation and size you request, have to match exactly in your preload and UI requests. To debug, see http://bumptech.github.io/glide/doc/debugging.html#unexpected-cache-misses.

romshiri commented 7 years ago

Thanks, @sjudd! that's was really helpful and got things a little bit smoother. I still encounter an issue - when you scroll really fast you see "blank" images that haven't been preloaded yet - which is ok, as preloading takes some time. In that case, I'd like the regular "image loading" (the one that happens on view holder binding) to take effect immediately, without waiting to the preloading to finish first.

Unfortunately, it seems like when the view holder loads the photo, it waits for the other preloaded images in the queue to load first, resulting in a long delay since the view holder had appeared to the moment the image actually loaded.

I thought that setting Priority will take care of that, but it doesn't seem to work. This is the code I use to load and preload images as you suggested (thumbnail's size is the same for both methods):


// This method is used to load an image in a view holder binding. 
@Override
    public void loadThumbnail(Object source, ImageView targetView, int thumbnailSize) {
        int size = (int) (thumbnailSize * THUMBNAIL_SIZE_REDUCER);
        RequestOptions options = getRequestOptions(size, size, Priority.HIGH);
        GlideApp.with(context)
                .load(source)
                .apply(options)
                .dontAnimate()
                .into(targetView);
    }

// this method is used to preload images in the background as soon as possible.
    @Override
    public void preloadThumbnail(Object source, int thumbnailSize) {
        int size = (int) (thumbnailSize * THUMBNAIL_SIZE_REDUCER);
        RequestOptions options = getRequestOptions(size, size, Priority.LOW);
        GlideApp.with(context)
                .load(source)
                .apply(options)
                .dontAnimate()
                .preload();
    }

    private RequestOptions getRequestOptions(int width, int height, Priority priority) {
        return new RequestOptions()
                .centerCrop()
                .priority(priority)
                .override(width, height);
romshiri commented 7 years ago

Keep struggling with it. @sjudd any idea? any suggestions how can I trace it?

sjudd commented 7 years ago

You'll only want to use preload when the user isn't looking at your app (or at least the screen in question). Priority does matter, but it only matters for tasks in the Executor's queue. If you queue enough low priority items, all of the threads in the Executor may end up busy with low priority jobs. if you then add a high priority job, it will be put at the front of the queue, but it won't pre-empt the jobs already running on the executor.

One other thing you can do if you really want to have the preload requests running while the app or screen is open is to use Glide's Future APIs (submit) to limit the number of threads the preload requests can use. as an example, see the Flickr sample app: https://github.com/bumptech/glide/blob/e86fd41e16aac1b95884494c1097417b4ab15a5a/samples/flickr/src/main/java/com/bumptech/glide/samples/flickr/FlickrPhotoGrid.java#L58 https://github.com/bumptech/glide/blob/e86fd41e16aac1b95884494c1097417b4ab15a5a/samples/flickr/src/main/java/com/bumptech/glide/samples/flickr/FlickrSearchActivity.java#L313

stale[bot] commented 7 years ago

This issue has been automatically marked as stale because it has not had activity in the last seven days. It will be closed if no further activity occurs within the next seven days. Thank you for your contributions.

sevar83 commented 6 years ago

Just to leave some info for future visitors... What made a dramatic improvement for me was using the preloader code from the Flickr sample mentioned above combined with extra space layout manager described here: https://androiddevx.wordpress.com/2014/12/05/recycler-view-pre-cache-views/ One just need to change the code from LinearLayoutManager to GridLayoutManager. I think the only missing part to achieve perfect performance would be early preloading with JobScheduler of those images which are shown first. Typically the first ROWS * COLUMNS images in the adapter list. The preloader will take care for the items further in the list.

sjudd commented 6 years ago

If you're using Glide's preloading library I wouldn't expect that you'd need to use getExtraLayoutSpace. The preloading library should already take care of loading images in the direction the user is scrolling so that they're in memory by the time onBindViewHolder is called.

The disadvantage of something like getExtraLayoutSpace is that you're binding more views than necessary. Although in a steady state (while the user is scrolling) the cost isn't any higher, you're definitely incurring an extra penalty the first time your grid is displayed because you have to layout and bind that many more views.

Worth noting that RecyclerView also already implements prefetching and can attempt to bind views ahead in the direction the user is scrolling, which will more or less automatically prefetch images. ReyclerView's implementation is primarily aimed at eliminating jank, not fetching images, so it's less aggressive and less effective (for image loading) than Glide's image preloading library

sevar83 commented 6 years ago

I agree using extra space is hacky. But in my case preloading doesn't work without it. I have prefetching set up for my nested RVs. As far as I understood I don't need to set up prefetching for the simple non-nested RVs, right?