Skip to content

Commit 90b3b9f

Browse files
committed
Avoid clearing Bitmaps in use by paused GifDrawables.
Fixes #2526.
1 parent 2a0a594 commit 90b3b9f

File tree

2 files changed

+125
-12
lines changed

2 files changed

+125
-12
lines changed

library/src/main/java/com/bumptech/glide/load/resource/gif/GifFrameLoader.java

+26-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class GifFrameLoader {
4343
private DelayTarget next;
4444
private Bitmap firstFrame;
4545
private Transformation<Bitmap> transformation;
46+
private DelayTarget pendingTarget;
4647

4748
public interface FrameCallback {
4849
void onFrameReady();
@@ -181,6 +182,10 @@ void clear() {
181182
requestManager.clear(next);
182183
next = null;
183184
}
185+
if (pendingTarget != null) {
186+
requestManager.clear(pendingTarget);
187+
pendingTarget = null;
188+
}
184189
gifDecoder.clear();
185190
isCleared = true;
186191
}
@@ -194,9 +199,17 @@ private void loadNextFrame() {
194199
return;
195200
}
196201
if (startFromFirstFrame) {
202+
Preconditions.checkArgument(
203+
pendingTarget == null, "Pending target must be null when starting from the first frame");
197204
gifDecoder.resetFrameIndex();
198205
startFromFirstFrame = false;
199206
}
207+
if (pendingTarget != null) {
208+
DelayTarget temp = pendingTarget;
209+
pendingTarget = null;
210+
onFrameReady(temp);
211+
return;
212+
}
200213
isLoadPending = true;
201214
// Get the delay before incrementing the pointer because the delay indicates the amount of time
202215
// we want to spend on the current frame.
@@ -218,14 +231,27 @@ private void recycleFirstFrame() {
218231
void setNextStartFromFirstFrame() {
219232
Preconditions.checkArgument(!isRunning, "Can't restart a running animation");
220233
startFromFirstFrame = true;
234+
if (pendingTarget != null) {
235+
requestManager.clear(pendingTarget);
236+
pendingTarget = null;
237+
}
221238
}
222239

223240
// Visible for testing.
224241
void onFrameReady(DelayTarget delayTarget) {
242+
isLoadPending = false;
225243
if (isCleared) {
226244
handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, delayTarget).sendToTarget();
227245
return;
228246
}
247+
// If we're not running, notifying here will recycle the frame that we might currently be
248+
// showing, which breaks things (see #2526). We also can't discard this frame because we've
249+
// already incremented the frame pointer and can't decode the same frame again. Instead we'll
250+
// just hang on to this next frame until start() or clear() are called.
251+
if (!isRunning) {
252+
pendingTarget = delayTarget;
253+
return;
254+
}
229255

230256
if (delayTarget.getResource() != null) {
231257
recycleFirstFrame();
@@ -242,7 +268,6 @@ void onFrameReady(DelayTarget delayTarget) {
242268
}
243269
}
244270

245-
isLoadPending = false;
246271
loadNextFrame();
247272
}
248273

library/src/test/java/com/bumptech/glide/load/resource/gif/GifFrameLoaderTest.java

+99-11
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,16 @@ public void tearDown() {
7878
@NonNull
7979
private GifFrameLoader createGifFrameLoader(Handler handler) {
8080
Glide glide = getGlideSingleton();
81-
return new GifFrameLoader(
81+
GifFrameLoader result = new GifFrameLoader(
8282
glide.getBitmapPool(),
8383
requestManager,
8484
gifDecoder,
8585
handler,
8686
requestBuilder,
8787
transformation,
8888
firstFrame);
89+
result.subscribe(callback);
90+
return result;
8991
}
9092

9193
private static Glide getGlideSingleton() {
@@ -95,10 +97,11 @@ private static Glide getGlideSingleton() {
9597
@SuppressWarnings("unchecked")
9698
@Test
9799
public void testSetFrameTransformationSetsTransformationOnRequestBuilder() {
100+
verify(requestBuilder, times(2)).apply(isA(RequestOptions.class));
98101
Transformation<Bitmap> transformation = mock(Transformation.class);
99102
loader.setFrameTransformation(transformation, firstFrame);
100103

101-
verify(requestBuilder, times(2)).apply(isA(RequestOptions.class));
104+
verify(requestBuilder, times(3)).apply(isA(RequestOptions.class));
102105
}
103106

104107
@Test(expected = NullPointerException.class)
@@ -115,15 +118,11 @@ public void testReturnsSizeFromGifDecoderAndCurrentFrame() {
115118

116119
@Test
117120
public void testStartGetsNextFrameIfNotStartedAndWithNoLoadPending() {
118-
loader.subscribe(callback);
119-
120121
verify(requestBuilder).into(aTarget());
121122
}
122123

123124
@Test
124125
public void testGetNextFrameIncrementsSignatureAndAdvancesDecoderBeforeStartingLoad() {
125-
loader.subscribe(callback);
126-
127126
InOrder order = inOrder(gifDecoder, requestBuilder);
128127
order.verify(gifDecoder).advance();
129128
order.verify(requestBuilder).apply(isA(RequestOptions.class));
@@ -147,22 +146,22 @@ public void testGetCurrentFrameReturnsCurrentBitmapAfterLoadHasCompleted() {
147146

148147
@Test
149148
public void testStartDoesNotStartIfAlreadyRunning() {
150-
loader.subscribe(callback);
151149
loader.subscribe(mock(FrameCallback.class));
152150

153151
verify(requestBuilder, times(1)).into(aTarget());
154152
}
155153

156154
@Test
157155
public void testGetNextFrameDoesNotStartLoadIfLoaderIsNotRunning() {
156+
verify(requestBuilder, times(1)).into(aTarget());
157+
loader.unsubscribe(callback);
158158
loader.onFrameReady(mock(DelayTarget.class));
159159

160-
verify(requestBuilder, never()).into(aTarget());
160+
verify(requestBuilder, times(1)).into(aTarget());
161161
}
162162

163163
@Test
164164
public void testGetNextFrameDoesNotStartLoadIfLoadIsInProgress() {
165-
loader.subscribe(callback);
166165
loader.unsubscribe(callback);
167166
loader.subscribe(callback);
168167

@@ -171,7 +170,6 @@ public void testGetNextFrameDoesNotStartLoadIfLoadIsInProgress() {
171170

172171
@Test
173172
public void testGetNextFrameDoesStartLoadIfRestartedAndNoLoadIsInProgress() {
174-
loader.subscribe(callback);
175173
loader.unsubscribe(callback);
176174

177175
loader.onFrameReady(mock(DelayTarget.class));
@@ -182,7 +180,6 @@ public void testGetNextFrameDoesStartLoadIfRestartedAndNoLoadIsInProgress() {
182180

183181
@Test
184182
public void testGetNextFrameDoesStartLoadAfterLoadCompletesIfStarted() {
185-
loader.subscribe(callback);
186183
loader.onFrameReady(mock(DelayTarget.class));
187184

188185
verify(requestBuilder, times(2)).into(aTarget());
@@ -269,6 +266,97 @@ public void testClearsCompletedLoadOnFrameReadyIfCleared() {
269266
assertNull(loader.getCurrentFrame());
270267
}
271268

269+
@Test
270+
public void onFrameReady_whenNotRunning_doesNotClearPreviouslyLoadedImage() {
271+
loader = createGifFrameLoader(/*handler=*/ null);
272+
DelayTarget loaded = mock(DelayTarget.class);
273+
when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
274+
loader.onFrameReady(loaded);
275+
loader.unsubscribe(callback);
276+
277+
DelayTarget nextFrame = mock(DelayTarget.class);
278+
when(nextFrame.getResource())
279+
.thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
280+
loader.onFrameReady(nextFrame);
281+
verify(requestManager, never()).clear(loaded);
282+
}
283+
284+
@Test
285+
public void onFrameReady_whenNotRunning_clearsPendingFrameOnClear() {
286+
loader = createGifFrameLoader(/*handler=*/ null);
287+
DelayTarget loaded = mock(DelayTarget.class);
288+
when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
289+
loader.onFrameReady(loaded);
290+
loader.unsubscribe(callback);
291+
292+
DelayTarget nextFrame = mock(DelayTarget.class);
293+
when(nextFrame.getResource())
294+
.thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
295+
loader.onFrameReady(nextFrame);
296+
297+
loader.clear();
298+
verify(requestManager).clear(loaded);
299+
verify(requestManager).clear(nextFrame);
300+
}
301+
302+
@Test
303+
public void onFrameReady_whenNotRunning_clearsOldFrameOnStart() {
304+
loader = createGifFrameLoader(/*handler=*/ null);
305+
DelayTarget loaded = mock(DelayTarget.class);
306+
when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
307+
loader.onFrameReady(loaded);
308+
loader.unsubscribe(callback);
309+
310+
DelayTarget nextFrame = mock(DelayTarget.class);
311+
when(nextFrame.getResource())
312+
.thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
313+
loader.onFrameReady(nextFrame);
314+
315+
loader.subscribe(callback);
316+
verify(requestManager).clear(loaded);
317+
}
318+
319+
@Test
320+
public void onFrameReady_whenNotRunning_callsFrameReadyWithNewFrameOnStart() {
321+
loader = createGifFrameLoader(/*handler=*/ null);
322+
DelayTarget loaded = mock(DelayTarget.class);
323+
when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
324+
loader.onFrameReady(loaded);
325+
loader.unsubscribe(callback);
326+
327+
DelayTarget nextFrame = mock(DelayTarget.class);
328+
Bitmap expected = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
329+
when(nextFrame.getResource())
330+
.thenReturn(expected);
331+
loader.onFrameReady(nextFrame);
332+
333+
verify(callback, times(1)).onFrameReady();
334+
loader.subscribe(callback);
335+
verify(callback, times(2)).onFrameReady();
336+
assertThat(loader.getCurrentFrame()).isEqualTo(expected);
337+
}
338+
339+
@Test
340+
public void startFromFirstFrame_withPendingFrame_clearsPendingFrame() {
341+
loader = createGifFrameLoader(/*handler=*/ null);
342+
DelayTarget loaded = mock(DelayTarget.class);
343+
when(loaded.getResource()).thenReturn(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888));
344+
loader.onFrameReady(loaded);
345+
loader.unsubscribe(callback);
346+
347+
DelayTarget nextFrame = mock(DelayTarget.class);
348+
Bitmap expected = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
349+
when(nextFrame.getResource())
350+
.thenReturn(expected);
351+
loader.onFrameReady(nextFrame);
352+
353+
loader.setNextStartFromFirstFrame();
354+
verify(requestManager).clear(nextFrame);
355+
356+
loader.subscribe(callback);
357+
verify(callback, times(1)).onFrameReady();
358+
}
359+
272360
@SuppressWarnings("unchecked")
273361
private static Target<Bitmap> aTarget() {
274362
return isA(Target.class);

0 commit comments

Comments
 (0)