Skip to content

Commit 4affb8d

Browse files
sjuddglide-copybara-robot
authored andcommittedJan 6, 2023
Register RequestManagers before adding Lifecycle listeners.
RequestManagers are registered to a singleton. If they're not unregistered, we'll leak the RequestManager and the corresponding Activity or Fragment. Unregistration is done via the destroyed Lifecycle event. While we assert that the Activity is not destroyed at a higher level, that assertion will not trigger during onDestroy calls on API 29+. As a result on API 29+, if we add a listener and then register the RequestManager statically, we'll end up triggering an assertion because the RequestManager will be unregistered before it is registered. Pre API 29 we'd have crashed earlier due to the Activity having been destroyed. To fix this on API 29+, we'll move the registration earlier so that we always register before there's a chance that the Lifecycle will trigger unregistration. This behavior changed a bit when we swapped over to using Android's Lifecycle. However I think the old behavior would simply have registered the RequestManager and never unregistered it, leading to a memory leak. PiperOrigin-RevId: 500018277
1 parent 0f75576 commit 4affb8d

File tree

2 files changed

+76
-2
lines changed

2 files changed

+76
-2
lines changed
 

‎instrumentation/src/androidTest/java/com/bumptech/glide/RequestManagerLifecycleTest.java

+72
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
import static com.google.common.truth.Truth.assertThat;
44
import static org.junit.Assert.assertThrows;
5+
import static org.junit.Assert.fail;
6+
import static org.junit.Assume.assumeTrue;
57

8+
import android.os.Build;
69
import android.os.Bundle;
710
import android.view.LayoutInflater;
811
import android.view.View;
@@ -13,14 +16,19 @@
1316
import androidx.fragment.app.FragmentActivity;
1417
import androidx.fragment.app.FragmentManager;
1518
import androidx.fragment.app.testing.FragmentScenario;
19+
import androidx.lifecycle.Lifecycle.Event;
1620
import androidx.lifecycle.Lifecycle.State;
21+
import androidx.lifecycle.LifecycleObserver;
22+
import androidx.lifecycle.LifecycleOwner;
23+
import androidx.lifecycle.OnLifecycleEvent;
1724
import androidx.test.core.app.ActivityScenario;
1825
import androidx.test.core.app.ActivityScenario.ActivityAction;
1926
import androidx.test.ext.junit.rules.ActivityScenarioRule;
2027
import androidx.test.ext.junit.runners.AndroidJUnit4;
2128
import com.bumptech.glide.instrumentation.R;
2229
import com.bumptech.glide.test.DefaultFragmentActivity;
2330
import com.bumptech.glide.testutil.TearDownGlide;
31+
import java.util.concurrent.atomic.AtomicReference;
2432
import org.junit.Before;
2533
import org.junit.Rule;
2634
import org.junit.Test;
@@ -59,6 +67,70 @@ public void get_withActivityBeforeCreate_startsRequestManager() {
5967
scenario.onActivity(activity -> assertThat(Glide.with(activity).isPaused()).isFalse());
6068
}
6169

70+
// See b/262668610
71+
@SuppressWarnings("OnLifecycleEvent")
72+
@Test
73+
public void get_withActivityOnDestroy_QPlus_doesNotCrash() {
74+
// Activity#isDestroyed's behavior seems to have changed in Q. On Q+, isDestroyed returns false
75+
// during onDestroy, so we have to handle that case explicitly.
76+
assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q);
77+
scenario.moveToState(State.CREATED);
78+
79+
class GetOnDestroy implements LifecycleObserver {
80+
private final FragmentActivity activity;
81+
82+
GetOnDestroy(FragmentActivity activity) {
83+
this.activity = activity;
84+
}
85+
86+
@OnLifecycleEvent(Event.ON_DESTROY)
87+
public void onDestroy(@NonNull LifecycleOwner owner) {
88+
Glide.with(activity);
89+
}
90+
}
91+
scenario.onActivity(
92+
activity -> activity.getLifecycle().addObserver(new GetOnDestroy(activity)));
93+
scenario.moveToState(State.DESTROYED);
94+
}
95+
96+
@SuppressWarnings("OnLifecycleEvent")
97+
@Test
98+
public void get_withActivityOnDestroy_afterJellyBeanAndbeforeQ_doesNotCrash() {
99+
// Activity#isDestroyed's behavior seems to have changed in Q. On <Q, isDestroyed returns true
100+
// during onDestroy, triggering an assertion in Glide. < Jelly bean, isDestroyed is not
101+
// available as a method.
102+
assumeTrue(
103+
Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN
104+
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.Q);
105+
AtomicReference<Exception> thrownException = new AtomicReference<>();
106+
scenario.moveToState(State.CREATED);
107+
108+
class GetOnDestroy implements LifecycleObserver {
109+
private final FragmentActivity activity;
110+
111+
GetOnDestroy(FragmentActivity activity) {
112+
this.activity = activity;
113+
}
114+
115+
@OnLifecycleEvent(Event.ON_DESTROY)
116+
public void onDestroy(@NonNull LifecycleOwner owner) {
117+
try {
118+
Glide.with(activity);
119+
fail("Failed to throw expected exception");
120+
} catch (Exception e) {
121+
thrownException.set(e);
122+
}
123+
}
124+
}
125+
scenario.onActivity(
126+
activity -> activity.getLifecycle().addObserver(new GetOnDestroy(activity)));
127+
scenario.moveToState(State.DESTROYED);
128+
129+
assertThat(thrownException.get())
130+
.hasMessageThat()
131+
.contains("You cannot start a load for a destroyed activity");
132+
}
133+
62134
@Test
63135
public void get_withFragment_beforeFragmentIsAdded_throws() {
64136
Fragment fragment = new Fragment();

‎library/src/main/java/com/bumptech/glide/RequestManager.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ public RequestManager(
129129
context.getApplicationContext(),
130130
new RequestManagerConnectivityListener(requestTracker));
131131

132+
// Order matters, this might be unregistered by teh listeners below, so we need to be sure to
133+
// register first to prevent both assertions and memory leaks.
134+
glide.registerRequestManager(this);
135+
132136
// If we're the application level request manager, we may be created on a background thread.
133137
// In that case we cannot risk synchronously pausing or resuming requests, so we hack around the
134138
// issue by delaying adding ourselves as a lifecycle listener by posting to the main thread.
@@ -143,8 +147,6 @@ public RequestManager(
143147
defaultRequestListeners =
144148
new CopyOnWriteArrayList<>(glide.getGlideContext().getDefaultRequestListeners());
145149
setRequestOptions(glide.getGlideContext().getDefaultRequestOptions());
146-
147-
glide.registerRequestManager(this);
148150
}
149151

150152
protected synchronized void setRequestOptions(@NonNull RequestOptions toSet) {

0 commit comments

Comments
 (0)
Please sign in to comment.