Skip to content

Commit

Permalink
Avoid unsafely recycling Bitmaps in DrawableTransformation.
Browse files Browse the repository at this point in the history
There were two issues here:
1. The BitmapPool used by the Drawable converter was recycling Bitmaps
when it was used specifically to avoid recycling Bitmaps when the converter didn’t allocate the returned values
2. If the Drawable wasn’t transformed, because the BitmapTransformation that the DrawableTransformation wrapped didn’t need to mutate the 
converted Drawable, the intermediate Bitmap would be placed into the
BitmapPool twice.

Both of these issues should be fixed with some additional tests to 
hopefully prevent this from happening again.
  • Loading branch information
sjudd committed Nov 2, 2017
1 parent 5733543 commit 2371aa6
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.TransformationUtils;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.test.BitmapSubject;
import com.bumptech.glide.test.GlideApp;
import java.util.concurrent.ExecutionException;
import org.junit.After;
import org.junit.Before;
Expand Down Expand Up @@ -125,4 +127,83 @@ public void load_withColorDrawable_sizeOriginal_requiredTransform_fails()
.submit()
.get();
}

@Test
public void load_withBitmapDrawable_andDoNothingTransformation_doesNotRecycleBitmap()
throws ExecutionException, InterruptedException {
Bitmap bitmap = Bitmap.createBitmap(100, 200, Config.ARGB_8888);
BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap);

Drawable result = GlideApp.with(context)
.load(drawable)
.fitCenter()
.override(bitmap.getWidth(), bitmap.getHeight())
.submit()
.get();

BitmapSubject.assertThat(result).isNotRecycled();
}

@Test
public void load_withBitmapDrawable_andFunctionalTransformation_doesNotRecycleBitmap()
throws ExecutionException, InterruptedException {
Bitmap bitmap = Bitmap.createBitmap(100, 200, Config.ARGB_8888);
BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap);

Drawable result = GlideApp.with(context)
.load(drawable)
.fitCenter()
.override(bitmap.getWidth() / 2, bitmap.getHeight() / 2)
.submit()
.get();

BitmapSubject.assertThat(result).isNotRecycled();
}

@Test
public void load_withColorDrawable_fixedSize_unitBitmapTransform_recyclesIntermediates()
throws ExecutionException, InterruptedException {
Drawable colorDrawable = new ColorDrawable(Color.RED);

int width = 100;
int height = 200;

GlideApp.with(context)
.load(colorDrawable)
.fitCenter()
.override(width, height)
.submit()
.get();

BitmapPool bitmapPool = Glide.get(context).getBitmapPool();
// Make sure we didn't put the same Bitmap twice.
Bitmap first = bitmapPool.get(width, height, Config.ARGB_8888);
Bitmap second = bitmapPool.get(width, height, Config.ARGB_8888);

assertThat(first).isNotSameAs(second);
}
@Test
public void load_withColorDrawable_fixedSize_functionalBitmapTransform_doesNotRecycleOutput()
throws ExecutionException, InterruptedException {
Drawable colorDrawable = new ColorDrawable(Color.RED);

int width = 100;
int height = 200;

Drawable result = GlideApp.with(context)
.load(colorDrawable)
.circleCrop()
.override(width, height)
.submit()
.get();

BitmapSubject.assertThat(result).isNotRecycled();

BitmapPool bitmapPool = Glide.get(context).getBitmapPool();
// Make sure we didn't put the same Bitmap twice.
Bitmap first = bitmapPool.get(width, height, Config.ARGB_8888);
Bitmap second = bitmapPool.get(width, height, Config.ARGB_8888);

assertThat(first).isNotSameAs(second);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/**
* Truth assertions for comparing {@link Bitmap}s.
*/
final class BitmapSubject extends Subject<BitmapSubject, Bitmap> {
public final class BitmapSubject extends Subject<BitmapSubject, Bitmap> {

private static final SubjectFactory<BitmapSubject, Bitmap> FACTORY =
new SubjectFactory<BitmapSubject, Bitmap>() {
Expand All @@ -30,14 +30,14 @@ private BitmapSubject(FailureStrategy failureStrategy,
super(failureStrategy, subject);
}

static BitmapSubject assertThat(Drawable drawable) {
public static BitmapSubject assertThat(Drawable drawable) {
if (!(drawable instanceof BitmapDrawable)) {
throw new IllegalArgumentException("Not a BitmapDrawable: " + drawable);
}
return assertThat(((BitmapDrawable) drawable).getBitmap());
}

static BitmapSubject assertThat(Bitmap bitmap) {
public static BitmapSubject assertThat(Bitmap bitmap) {
return Truth.assertAbout(FACTORY).that(bitmap);
}

Expand All @@ -54,34 +54,40 @@ private static String getDisplayString(Bitmap bitmap) {
+ ">";
}

void sameAs(@DrawableRes int resourceId) {
public void sameAs(@DrawableRes int resourceId) {
Context context = InstrumentationRegistry.getTargetContext();
Drawable drawable =
ResourcesCompat.getDrawable(context.getResources(), resourceId, context.getTheme());
sameAs(drawable);
}

void isMutable() {
public void isMutable() {
if (!getSubject().isMutable()) {
fail("is mutable");
}
}

void isImmutable() {
public void isImmutable() {
if (getSubject().isMutable()) {
fail("is immutable");
}
}

public void isNotRecycled() {
if (getSubject().isRecycled()) {
fail("is not recycled");
}
}

@SuppressWarnings("unchecked")
void sameAs(Drawable other) {
public void sameAs(Drawable other) {
if (!(other instanceof BitmapDrawable)) {
fail("Not a BitmapDrawable");
}
sameAs(((BitmapDrawable) other).getBitmap());
}

void sameAs(Bitmap other) {
public void sameAs(Bitmap other) {
if (!getSubject().sameAs(other)) {
fail("is the same as " + getDisplayString(other));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@

final class DrawableToBitmapConverter {
private static final String TAG = "DrawableToBitmap";
private static final BitmapPool NO_BITMAP_POOL = new BitmapPoolAdapter();
private static final BitmapPool NO_RECYCLE_BITMAP_POOL = new BitmapPoolAdapter() {
@Override
public void put(Bitmap bitmap) {
// Avoid calling super to avoid recycling the given Bitmap.
}
};
private DrawableToBitmapConverter() {
// Utility class.
}
Expand All @@ -34,7 +39,7 @@ static Resource<Bitmap> convert(BitmapPool bitmapPool, Drawable drawable, int wi
isRecycleable = true;
}

BitmapPool toUse = isRecycleable ? bitmapPool : NO_BITMAP_POOL;
BitmapPool toUse = isRecycleable ? bitmapPool : NO_RECYCLE_BITMAP_POOL;
return BitmapResource.obtain(result, toUse);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ public Resource<Drawable> transform(Context context, Resource<Drawable> resource

if (transformedBitmapResource.equals(bitmapResourceToTransform)) {
transformedBitmapResource.recycle();
bitmapResourceToTransform.recycle();
return resource;
} else {
return newDrawableResource(context, transformedBitmapResource.get());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.bumptech.glide.load.resource.bitmap;

import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter;
import com.bumptech.glide.load.resource.SimpleResource;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 18)
public class DrawableTransformationTest {

private BitmapPool bitmapPool;
@Mock private Transformation<Bitmap> bitmapTransformation;
private DrawableTransformation transformation;
private Context context;

@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
transformation = new DrawableTransformation(bitmapTransformation, /*isRequired=*/ true);
context = RuntimeEnvironment.application;
bitmapPool = new BitmapPoolAdapter();
Glide.init(new GlideBuilder()
.setBitmapPool(bitmapPool)
.build(context));
}

@After
public void tearDown() {
Glide.tearDown();
}

@Test
public void transform_withBitmapDrawable_andUnitBitmapTransformation_doesNotRecycle() {
when(
bitmapTransformation
.transform(
any(Context.class), anyBitmapResource(), anyInt(), anyInt()))
.thenAnswer(new ReturnGivenResource());

Bitmap bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap);
@SuppressWarnings("unchecked")
Resource<Drawable> input =
(Resource<Drawable>) (Resource<?>) new BitmapDrawableResource(drawable, bitmapPool);
transformation.transform(context, input, /*outWidth=*/ 100, /*outHeight=*/ 200);

assertThat(bitmap.isRecycled()).isFalse();
}

@Test
public void transform_withBitmapDrawable_andFunctionalBitmapTransformation_doesNotRecycle() {
when(bitmapTransformation.transform(
any(Context.class), anyBitmapResource(), anyInt(), anyInt()))
.thenAnswer(new Answer<Resource<Bitmap>>() {
@Override
public Resource<Bitmap> answer(InvocationOnMock invocationOnMock) throws Throwable {
return BitmapResource.obtain(
Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888), bitmapPool);
}
});
Bitmap bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap);
@SuppressWarnings("unchecked")
Resource<Drawable> input =
(Resource<Drawable>) (Resource<?>) new BitmapDrawableResource(drawable, bitmapPool);
transformation.transform(context, input, /*outWidth=*/ 100, /*outHeight=*/ 200);

assertThat(bitmap.isRecycled()).isFalse();
}

@Test
public void transform_withColorDrawable_andUnitBitmapTransformation_recycles() {
bitmapPool = mock(BitmapPool.class);
Glide.tearDown();
Glide.init(new GlideBuilder().setBitmapPool(bitmapPool).build(context));
when(
bitmapTransformation
.transform(
any(Context.class), anyBitmapResource(), anyInt(), anyInt()))
.thenAnswer(new ReturnGivenResource());

ColorDrawable colorDrawable = new ColorDrawable(Color.RED);
final Resource<Drawable> input = new SimpleResource<Drawable>(colorDrawable);

doAnswer(new Answer() {
@Override
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
Bitmap bitmap = (Bitmap) invocationOnMock.getArguments()[0];
assertThat(bitmap.getWidth()).isEqualTo(100);
assertThat(bitmap.getHeight()).isEqualTo(200);
return null;
}
}).when(bitmapPool).put(any(Bitmap.class));
when(bitmapPool.get(anyInt(), anyInt(), any(Bitmap.Config.class)))
.thenAnswer(new Answer<Bitmap>() {
@Override
public Bitmap answer(InvocationOnMock invocationOnMock) throws Throwable {
int width = (Integer) invocationOnMock.getArguments()[0];
int height = (Integer) invocationOnMock.getArguments()[1];
Bitmap.Config config = (Bitmap.Config) invocationOnMock.getArguments()[2];
return Bitmap.createBitmap(width, height, config);
}
});

transformation.transform(context, input, /*outWidth=*/ 100, /*outHeight=*/ 200);

verify(bitmapPool).put(isA(Bitmap.class));
}

@SuppressWarnings("unchecked")
private static Resource<Bitmap> anyBitmapResource() {
return any(Resource.class);
}

private static final class ReturnGivenResource implements Answer<Resource<Bitmap>> {

@Override
public Resource<Bitmap> answer(InvocationOnMock invocationOnMock) throws Throwable {
@SuppressWarnings("unchecked")
Resource<Bitmap> input = (Resource<Bitmap>) invocationOnMock.getArguments()[1];
return input;
}
}
}

0 comments on commit 2371aa6

Please sign in to comment.