Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Glide doesn't show the latest image if its from the same URL #1847

Closed
Parithi opened this issue Apr 10, 2017 · 15 comments
Closed

Glide doesn't show the latest image if its from the same URL #1847

Parithi opened this issue Apr 10, 2017 · 15 comments
Labels

Comments

@Parithi
Copy link

Parithi commented Apr 10, 2017

Hi,

I could find that this issue has been reported by a few others but I would like to have a clearer solution for this. I'm new to Glide. Kindly bear with me.

Issue :
I'm loading an image from the server where the image will get updated with the same url in a listview. Glide is showing the old cached image even if the image has been updated from the server.

Things I've read and tried:

  1. Adding Signature to the request by converting the currentTime milliseconds as a string. - If I used the signature with the current timestamp, the request is being called every single time making the cache useless. (found using the logginglistener - isFromMemoryCache is always false on all requests)
  2. Disabling the memory and disk caches. - I cannot afford to do this, I believe there should be a better way for this.
  3. Invalidating the imageview and setting its image resource to null before sending the request.

How I need to solve it:
I would like to send a request to the image URL, obtain the headers first, check if the timestamp/etag from the header request are different from the cached local copy. If they are different, send a request again to obtain the full image data and update the cache.

Does the signature() api follow this method internally within Glide? If not, which classes do I need to change by forking the library in order to accomplish my goal.

Code used :
Glide.with(this).load("http://parithinetwork.com/test.jpg").into(imageView);

Glide Version: 3.7.0

Integration libraries: None

Device/Android Version: Xperia Z2(6.0.1), Samsung S3 (4.1)

@TWiStErRob
Copy link
Collaborator

TWiStErRob commented Apr 12, 2017

@Parithi nice report/question!

Re .signature() vs headers: signature is not that clever, it's just a way to salt the disk cache key.

  1. You missed something with solution 1), divide the current time in millis by a period (say 24*60*60*1000), so you get a day: the number will change every day, not every millisecond. This means that the image will be refreshed every day, not every single time. This is a good approach for profile images, because they're not that important, especially if they're someone else's in a forum or chat.

  2. If you want to do a HEAD before GET, consider replacing the integration library from the default HttpUrlFetcher to something custom or say OkHttp 3, where you can implement however much caching and magic you can squeeze out of the OkHttpClient. See https://github.com/bumptech/glide/wiki/Integration-Libraries for basics and source code of integration libraries for details (they're 2-4 classes only). The trick here is that you configure the OkHttp client as if you were using it directly. If you can get it to refresh the cache based on ETag for example, then you can disable Glide's disk cache, so it always passes through to OkHttp, in which case OkHttp will prevent going to the network if the file is in its cache.

  3. hacking Glide is not recommended, and also that trick shouldn't have any effect on caching.

  4. You didn't mention one of the best solutions: you're getting the url for the images from somewhere. That somewhere could send you metadata like image size and last modified time. If you use the lastModified sent by the server you get the latest image all the time, think like this:

    Glide.with(this).load(imageSource.url).signature(new StringSignature(imageSource.lastModified)).into(imageView);

    This way you can use all of Glide's features and get an up-to-date image when it changes.

@Parithi
Copy link
Author

Parithi commented Apr 13, 2017

Thanks a lot for the detailed help @TWiStErRob!

Regarding your answers, I have a few doubts.

Answer 1. My client's requirement is that I need to refresh the image immediately, so I can't have it to update once a day.

Answer 2. I believe this is the solution I should be going with but I'm not able to understand how this works.
From what I understand from the article you had provided, Glide will be using HttpUrlFetcher by default and we can use other libraries if needed.

In our app, we are currently using Okhttp 3.

I looked up the classes at: https://github.com/bumptech/glide/tree/master/integration/okhttp3/src/main/java/com/bumptech/glide/integration/okhttp3

So, once I add the code below to my build.gradle, Glide will be making the requests using OkHttp 3.

dependencies {
    compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
    ...
}

From your answer,

The trick here is that you configure the OkHttp client as if you were using it directly. If you can get it to refresh the cache based on ETag for example, then you can disable Glide's disk cache, so it always passes through to OkHttp, in which case OkHttp will prevent going to the network if the file is in its cache.

I don't understand this part. From what I understand,

Are you suggesting that I make changes to the Integration library files of Glide by performing the below?

If I'm making the Image HEAD Request using OkHttp client first, check if the ETag is changed, and update the cache. What methods does Glide provide me to do this?

In order to accomplish this, I need to

makeRequestToURLUsingOkHttp(URL){

        @Override
        onHeadersReceived(headers){
              if(ETAG from headers == Glide's Cached File ETAG){
                          // Load data from Glide's Cache
              } else {
                        // Make a new request using glide
               }
        }

}

or

Without adding integration libraries to Glide, I could use just make an Okhttp HEAD request for the URL, and obtain the ETAG and use it as a signature for the Glide request. Would this be a better way of doing it?

Answer 4. This solution was pretty obvious when I learnt about signature but My client doesn't/can't send me any kind of metadata, I have nothing but the static URL that doesn't change so I don't have any data to be used as a signature.

Thanks a lot for helping me out.

@TWiStErRob
Copy link
Collaborator

You're likely thinking too low level, you probably don't even need to do an actual HEAD, sorry if I misled you there.

dependencies {
    compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
}
public class CustomOkHttpGlideModule implements GlideModule {
	@Override public void applyOptions(Context context, GlideBuilder builder) {}
	@Override public void registerComponents(Context context, Glide glide) {
		OkHttpClient client = new OkHttpClient.Builder()
				// do some OkHttp 3 magic here to handle ETags (can't help you here, sorry)
				.build();
		glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));
	}
}
<meta-data android:name="your.package.CustomOkHttpGlideModule" android:value="GlideModule" />
<meta-data android:name="com.bumptech.glide.integration.okhttp3.OkHttpGlideModule" tools:node="remove" />
Glide.with(this).load("http://parithinetwork.com/test.jpg").diskCacheStrategy(NONE).into(imageView);

I suggest using Stetho, Wireshark or other advanced logging mechanism to your requests hitting the OkHttp cache.

I need to refresh the image immediately

I hope that allows for "keep the same image during a session", otherwise you need to turn off memory caching as well, which would be very unfortunate.

I could use just make an Okhttp HEAD request for the URL, and obtain the ETAG and use it as a signature for the Glide request.

That would also work as well, but as far as I know the above solution would work with one request instead of two per load.

@Parithi
Copy link
Author

Parithi commented Apr 17, 2017

Thanks for the great help @TWiStErRob! I'll try it out :) Please do consider integrating this directly into the next version of glide, if you think it would be a valuable addition. Thanks again!

@Parithi Parithi closed this as completed Apr 17, 2017
@tobiasKaminsky
Copy link

@TWiStErRob sorry for re-opening such an old issue, but I have a question on your idea:

Glide.with(this).load("http://parithinetwork.com/test.jpg").diskCacheStrategy(NONE).into(imageView);

DiskCacheStrategy.NONE leads to have none image cached, even if the etag is not changed.
Having another cache strategy, will result in no call.

This is how I have it implemented without glide:

  • download image on first call
    • cache it
    • store etag
  • on second call: use "if-none-match" header with etag
    • if 200: use data as image
    • if 304: re-use old cached image

This way, I have only 1 call, with either no data (304) or directly the data I can use (200).

So, back to glide: even if I add eTag to a DataFetcher, what shall I do then if I get 304 back? I only have callback.onDataReady() and callback.onDataFailed().
Both is not true…

@TWiStErRob
Copy link
Collaborator

If the fetcher is called it's too late to decide about caching. Fetcher fetches the data when it wasn't cached. You can disable caching in Glide and enable it in OkHttp (handling E-Tags), so it'll always call the fetcher, but OkHttp will use its own disk cache to respond.

@tobiasKaminsky
Copy link

This is how I solved it:

  • create a get call with "if-none-match" header and with latest stored eTag
  • handle return
    • 200 (new image): parse new eTag, get inputstream
    • 304 (unchanged): do nothing, inputSttream null
  • pass new/old eTag with inputstream to custom glide model fetcher
    • on 200: input stream is stored with new object key
    • on 304: input stream (null) is ignored as we already have the cached image, specified by key

This way, I can use glide with eTags and glides internal caching system and "any" network library.

@sjudd
Copy link
Collaborator

sjudd commented Aug 9, 2018

@tobiasKaminsky How do you deal with disk caching when you use the if-none-match header? If Glide already has your image in the disk cache, it never calls the corresponding data fetcher?

@tobiasKaminsky
Copy link

I am always querying with if-none-match to get the latest etag.
Only after that I call glide.
If I know that the file is in cache, I use .onlyRetrieveFromCache(true)

@imspatni
Copy link

@TWiStErRob thanks for the pointers 1,2,3,4 above and sorry for reopening it again. I'm curious about last 4th option where you asked about to add signature api.

  1. StringSignature is deprecated but found ObjectKey in replace of it.
  2. To add lastModified I'm storing it form previous header response and passing it like this.
GlideApp.with(context)
                .load(imageUrl)
                .signature(ObjectKey(lastModifiedDate))
                .into(imageView)

Now problem it, it still taking images from DATA_DISK_CACHE even after modified image.. I try by adding diskCacheStrategy(DiskCacheStrategy.NONE) but then it download image on every time restart application. Any clue around !!

@rakeshvihan
Copy link

Is it possible to force remote request after 'x' mins/hrs/days without invalidating disk cache?

@TWiStErRob
Copy link
Collaborator

@imspatni I think my answer is for v3, you're probably on v4, hence deprecation

@imspatni imageSource.lastModified meant that whereever you get your image list from, would tell you when the image was updated, and if that changes the extra signature would for the network request. With your impl it never changes, so you always hit the same. If you don't have a changing source, you can still do a timed invalidation: .signature(ObjectKey(System.currentTimeMillis() / 24*60*60*1000))

@rakesh-mohan the disk cache will contain the old image (if it was there), the whole question is when to invalidate, so I'm not sure I understand how you want to not invalidate. I can imagine something like this:

val time = System.currentTimeMillis()
Glide
    .with(this)
    .load(thing.url)
    .signature(ObjectKey(time / 24*60*60*1000))
    .thumbnail(Glide
        .with(this)
        .load(thing.url)
        .signature(ObjectKey(thing.lastRequestTimeStamp / 24*60*60*1000))
        .preventNetworkLoad() // not sure about the v4 API, in v3 it was not that bad
    )
    .into(...)
thing.lastRequestTimeStamp = time // this needs to be persisted for *each* of your things that have the image

@rakeshvihan
Copy link

@TWiStErRob Thanks much! This is the expected solution. It works 💯 .

@taichushouwang
Copy link

taichushouwang commented Dec 25, 2020

@TWiStErRob sorry for re-opening such an old issue, and I also face the same problem:
For our project, we also use a static url, so I can't get any kind of metadata for this request. And also this image is the avatar of user, so this is not suitable for this scenario. Because in a session, users can switch avatars as many times as they want.

I hope that allows for "keep the same image during a session", otherwise you need to turn off memory caching as well, which would be very unfortunate.

And for better performance and user experience, I also want to use cache.

So is there any way to get the image first, and then check its metadata like image last modified time or md5 to decide update ImageView or not.

Thanks a lot

Finally, I find a solution: #527 (comment)

@calvarez-ov
Copy link

@taichushouwang If I understand right, the workaround you mention is for a webcam image: they always want to reload a new image. They use the previous cached image as a placeholder, but always fetch a new image as well.

About the scenario when users can switch avatars as many times as they want, this isn't quite the same as a webcam. If a user didn't change their avatar, the client shouldn't reload a new one.

I'm looking for a solution to load a new image when it's available, without loading a new image when it's not necessary 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants