Skip to content

Crash when activity is destroyed #803

Closed
@michaelrhughes

Description

@michaelrhughes

TLDR; Glide should just log and cancel the load instead of crashing when the activity is destoryed since there is no point in loading anything. The user of the library shouldn't have to explicitly handle this situation.


Glide Version/Integration library (if any): 'com.github.bumptech.glide:glide:3.6.1'
Device/Android Version: Any/Android 5.0+
Issue details/Repro steps/Use case background:

This issue is related to issue #138. When making a call on glide if the activity has been destroyed it will throw an IllegalArgumentException. The following code in the RequestManagerRetriever.java class is responsible:

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private static void assertNotDestroyed(Activity activity) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) {
        throw new IllegalArgumentException("You cannot start a load for a destroyed activity");
    }
}

This is causing a crash in my crashlytics console for a small percentage of my users. There is likely some kind of timing issue where the user hits home as they are scrolling a big list and the activity is cleaned up which results in this crash. I could fix this issue by wrapping glide and running this code myself and simply not calling glide in this situation. However I believe this is the wrong approach, the library shouldn't be crashing in this situation and instead should log and do nothing.

If the activity is destroyed then the view that is being displayed which the image is being displayed is not visible and as a result no exception should be thrown. Instead glide should log and simply not show the image. Users of the library should not have to explicitly handle situations like these, it makes more sense to just handle it in the library.

Glide load line:

Glide.with(context).load(url).bitmapTransform(new GrayscaleTransformation(getContext())).into(imageView);

Layout XML: not relevant

Stack trace / LogCat:

Fatal Exception: java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity
       at com.bumptech.glide.manager.RequestManagerRetriever.assertNotDestroyed(RequestManagerRetriever.java:134)
       at com.bumptech.glide.manager.RequestManagerRetriever.get(RequestManagerRetriever.java:102)
       at com.bumptech.glide.manager.RequestManagerRetriever.get(RequestManagerRetriever.java:87)
       at com.bumptech.glide.Glide.with(Glide.java:620)
       at com.myapp.myapp$24.run(MyAppClass.java:977)
       at android.os.Handler.handleCallback(Handler.java:739)
       at android.os.Handler.dispatchMessage(Handler.java:95)
       at android.os.Looper.loop(Looper.java:135)
       at android.app.ActivityThread.main(ActivityThread.java:5431)
       at java.lang.reflect.Method.invoke(Method.java)
       at java.lang.reflect.Method.invoke(Method.java:372)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:914)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:707)

Activity

TWiStErRob

TWiStErRob commented on Dec 9, 2015

@TWiStErRob
Collaborator

Thanks very much for the detailed report!

What does com.myapp.myapp$24 do?
What triggers it to be scheduled for later execution?

michaelrhughes

michaelrhughes commented on Dec 9, 2015

@michaelrhughes
Author

It sounds like what you're getting at is that I can fix the issue on my end instead of making a change in the library. However I do not believe this is the best solution since people use a simplified library like Glide because they don't have to worry about corner cases like this. In my opinion Glide should handle this directly. Obviously I'm no expert on Glide's code so I could be off base here.

To answer your question a network call comes in which then posts to run the code on the UI thread. Since I am in a view there I can do:

onAttachedToWindow
    isAttached = true;

onDetachedFromWindow
    isAttached = false;

// in my method i can call
if (isAttached) {
    //Glide call
}

What do you guys think?

TWiStErRob

TWiStErRob commented on Dec 9, 2015

@TWiStErRob
Collaborator

You're right. I kind of agree with you, it would be simpler if Glide ignored this, but at the same time I don't, here's why. I think in general it's just good to know what's going on in your code. Glide ignoring this would hide a potential bug in your code, that you would consider a feature, but at the same time for someone else it may be a good thing that Glide brings it up. The problem is that there isn't really a good mid-way between log and a crash. Most people ignore logging, especially because Glide doesn't log much unless you turn it on explciitly. On the other hand if your app crashes, you're forced to find out why and fix the bug. Even if you could get away with not doing so.

I think in general if you're going from back->UI thread it's a good thing to check if the UI is still alive. Why would you spend any time updating something that doesn't exist, or not visible and never will be visible. Especially taking away from the precious render time and battery life. In this simple case you may not be doing much, but it's really easy to think of similar use cases which are resource heavy. So regarding your implementation idea, I think you can just ignore the whole Runnable ($24) if your activity.isFinishing(); or even better (together with isFinishing) try to cancel the network request if the activity dies.

By the way, to give a little background why that check is in place: Glide attaches a fragment to the activity/fragment if you're using the corresponding with(...). This fragment is used to subscribe to lifecycle events so requests can be automatically paused/resumed and cancelled. See http://stackoverflow.com/a/32887693/253468. Oh, and for this fragment to be added, as a normal one, the activity has to be in good condition. Follow the Glide.with call chain in code to see more.

michaelrhughes

michaelrhughes commented on Dec 9, 2015

@michaelrhughes
Author

Thanks for the detailed reply and spending time to look into this, I really appreciate it. It's amazing when devs support their own libraries with this level of support.

Glide is really powerful because, like you are saying, it can respect the lifecycle of activities and fragment. An activity being destroyed is a lifecycle event. Therefore I would argue it should respect and properly handle when your activity is destroyed.

I get where you are coming from but if Glide were to silently fail here the only additional overhead (in my app) would be a single post request which is negligible. This is a very small corner case in my code (13 crashes in 15000 sessions), I wouldn't have found it without crashlytics. Realistically adding ~6 lines of code (the activity destroyed check) isn't a big deal to work around it but I felt that Glide would be better as a library if it handled this situation.

TWiStErRob

TWiStErRob commented on Dec 9, 2015

@TWiStErRob
Collaborator

It's amazing when devs support their own libraries with this level of support.

I'm not even a full dev on Glide, I'm just having fun here on Glide issues having my daily dose of brain training. Sam's is the sole owner and main maintainer of Glide. I commit only minor things sometimes.

Considering what I just said, let's see what @sjudd's opinion is on ignoring a load on a destroyed activity with a warn log.

michaelrhughes

michaelrhughes commented on Dec 9, 2015

@michaelrhughes
Author

Thanks!

Guang1234567

Guang1234567 commented on Dec 10, 2015

@Guang1234567

@michaelrhughes
@TWiStErRob

Maybe you can solve this problem instead of using "activity.isFinishing()".

//In Activity
 RequestManager mRequestManager
  public void onCreate(){
    RequestManager mRequestManager= Glide.with(this);
     ....

   Runnable r = new Runnable(){
        mRequestManager.load(glideUrl).into(imageview).
   }

 //TODO:  post Runnable after activity is destoryed.
 }


//In Adapter
RecyclerAdapter(Context context, List<MediaStoreData> data, RequestManager requestManager) {
    this.data = data;
    mRequestManager= requestManager
}
TWiStErRob

TWiStErRob commented on Dec 10, 2015

@TWiStErRob
Collaborator

@lihanguang hmm, interesting idea. I'm not sure how it helps though. I mean I don't know whether this will:

  1. ignore any loads,
  2. go through with them unnecessarily,
  3. or just delay the crash for a later time.

These last two (2-3) sound bad, and my guess is that Glide will do 2. when called after destroy.

Guang1234567

Guang1234567 commented on Dec 11, 2015

@Guang1234567

@TWiStErRob
"Crash when activity is destroyed" means @michaelrhughes call Glide.with(this) after "activity is destoryed", so you can call Glide.with(this) at the certain moment that activity is not destoryed. In my solution above, "the certain moment" is onCreate()

Q1: ignore any loads.
Answer: Donot worry about that. Because i cache the RequestManager instance in onCreate(), then use this instance all the time . You can see the glide demo("com.bumptech.glide.samples.gallery.RecyclerAdapter") .

See RequestManagerFragment.onStart/onStop which calls ActivityFragmentLifecycle which propagates to RequestManager.onStart/onStop.

Q2: go through with them unnecessarily
Answer: "go through with them" means you donnot need to lazy get the instance of RequestManager that attached at the current Context( Activity). So i think "go through with them" in some case is necessarily.
For example: in a adapter ("com.bumptech.glide.samples.gallery.RecyclerAdapter")

//In Adapter
RecyclerAdapter(Context context, List<MediaStoreData> data, RequestManager requestManager) {
    this.data = data;
    mRequestManager= requestManager
}

@Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
      mRequestManager.load(glideurl).into(imageview);
}

Q3: or just delay the crash for a later time.
Answer: no delay crash, because see Q1 answer

TWiStErRob

TWiStErRob commented on Dec 11, 2015

@TWiStErRob
Collaborator

@lihanguang so it won't do any of what I said, simply because the RequestManager is paused when activity is destroyed, that's a good one :) It also eagerly initializes the magic fragment which prevents this and possible other weirdnesses.

I wonder if @sjudd did this in the samples knowingly. My guess would be that it just made sense to cache it and some APIs need to get it passed: for example the adapter you quoted benefits because it can be used in Activity/Fragment/or even with AppContext by removing the Glide.with dependency from inside adapter.bind and just passing the actual request manager.

@michaelrhughes did you try this?

sjudd

sjudd commented on Dec 12, 2015

@sjudd
Collaborator

Passing the RequestManager in rather than constantly calling Glide.with does a number of things:

  1. It makes the code testable (you can mock RequestManager)
  2. It allows you to start loads with the correct context for Views/Adapters created in Fragments (otherwise you end up with the View Context which points to the Activity, not the Fragment)
  3. It avoids the (relatively minimal) overhead of the with call itself

It's not intended to avoid this particular assertion, though it can be useful in limited cases where the assertion can't be avoided due to bugs in how fragments are handled in the support libraries (typically affects deeply nested Fragment hierarchies and/or fragment pagers).

The assertion exists because, aside from the fragment issues I mentioned, it never makes sense to try to start an image load while an Activity is being destroyed. It often indicates that you're doing something else dangerous, like starting an image load in an AsyncTask callback for a task that may finish after onDestroy is called and that isn't cancelled.

More practically, because Glide.with() returns a RequestManager the caller then uses, we'd have to create a special mock or pass along some state that indicates that the load should actually be ignored all the down to the into() call, which would be hard to maintain.

Guang1234567

Guang1234567 commented on Dec 17, 2015

@Guang1234567

Can using "RequestManager" to solve @michaelrhughes 's problem?

TWiStErRob

TWiStErRob commented on Dec 17, 2015

@TWiStErRob
Collaborator

@lihanguang Yes it was a good idea, though he didn't respond yet.

54 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @toby78@paulocoutinhox@sahandnayebaziz@ygnessin@sjudd

        Issue actions

          Crash when activity is destroyed · Issue #803 · bumptech/glide