Skip to content

Commit b0a7efe

Browse files
sjuddglide-copybara-robot
authored andcommittedOct 19, 2020
Add a method of frame waiting that resets when we get certain trim memory callbacks.
The GL context may be destroyed on TRIM_MEMORY_MODERATE or higher. We could strictly listen to that callback, but we'd end up racing because we only get trimMemory on the UI thread and decodes happen on multiple background threads. Hopefully by resetting when the app is backgrounded we can minimize those races. PiperOrigin-RevId: 337927814
1 parent 6d7c484 commit b0a7efe

File tree

11 files changed

+179
-48
lines changed

11 files changed

+179
-48
lines changed
 

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@
2828
import com.bumptech.glide.test.GlideApp;
2929
import com.bumptech.glide.test.ResourceIds;
3030
import com.bumptech.glide.test.TearDownGlide;
31+
import com.bumptech.glide.util.Util;
3132
import java.io.ByteArrayOutputStream;
3233
import java.io.FileNotFoundException;
3334
import java.io.IOException;
3435
import java.io.InputStream;
3536
import java.util.Locale;
37+
import java.util.concurrent.CountDownLatch;
38+
import java.util.concurrent.TimeUnit;
3639
import org.junit.Rule;
3740
import org.junit.Test;
3841
import org.junit.runner.RunWith;
@@ -117,11 +120,22 @@ public void loadTransparentGifResource_withNoOtherLoaders_decodesResource() {
117120
}
118121

119122
@Test
120-
public void loadTransparentGifResource_asHardware_withNoOtherLoaders_decodesResource() {
123+
public void loadTransparentGifResource_asHardware_withNoOtherLoaders_decodesResource()
124+
throws InterruptedException {
121125
assumeTrue(
122126
"Hardware Bitmaps are only supported on O+",
123127
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
124-
Glide.enableHardwareBitmaps();
128+
// enableHardwareBitmaps must be called on the main thread.
129+
final CountDownLatch latch = new CountDownLatch(1);
130+
Util.postOnUiThread(
131+
new Runnable() {
132+
@Override
133+
public void run() {
134+
Glide.enableHardwareBitmaps();
135+
latch.countDown();
136+
}
137+
});
138+
latch.await(5, TimeUnit.SECONDS);
125139

126140
Glide.get(context)
127141
.getRegistry()

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

+11-5
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
@SuppressWarnings("PMD.ImmutableField")
4242
public final class GlideBuilder {
4343
private final Map<Class<?>, TransitionOptions<?, ?>> defaultTransitionOptions = new ArrayMap<>();
44-
private final GlideExperiments.Builder glideExperiments = new GlideExperiments.Builder();
44+
private final GlideExperiments.Builder glideExperimentsBuilder = new GlideExperiments.Builder();
4545
private Engine engine;
4646
private BitmapPool bitmapPool;
4747
private ArrayPool arrayPool;
@@ -448,7 +448,7 @@ public GlideBuilder addGlobalRequestListener(@NonNull RequestListener<Object> li
448448
* <p>This is an experimental API that may be removed in the future.
449449
*/
450450
public GlideBuilder setLogRequestOrigins(boolean isEnabled) {
451-
glideExperiments.update(new LogRequestOrigins(), isEnabled);
451+
glideExperimentsBuilder.update(new LogRequestOrigins(), isEnabled);
452452
return this;
453453
}
454454

@@ -479,7 +479,7 @@ public GlideBuilder setLogRequestOrigins(boolean isEnabled) {
479479
* which may not agree.
480480
*/
481481
public GlideBuilder setImageDecoderEnabledForBitmaps(boolean isEnabled) {
482-
glideExperiments.update(
482+
glideExperimentsBuilder.update(
483483
new EnableImageDecoderForBitmaps(),
484484
/*isEnabled=*/ isEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q);
485485
return this;
@@ -556,8 +556,9 @@ Glide build(@NonNull Context context) {
556556
defaultRequestListeners = Collections.unmodifiableList(defaultRequestListeners);
557557
}
558558

559+
GlideExperiments experiments = glideExperimentsBuilder.build();
559560
RequestManagerRetriever requestManagerRetriever =
560-
new RequestManagerRetriever(requestManagerFactory);
561+
new RequestManagerRetriever(requestManagerFactory, experiments);
561562

562563
return new Glide(
563564
context,
@@ -571,7 +572,7 @@ Glide build(@NonNull Context context) {
571572
defaultRequestOptionsFactory,
572573
defaultTransitionOptions,
573574
defaultRequestListeners,
574-
glideExperiments.build());
575+
experiments);
575576
}
576577

577578
static final class ManualOverrideHardwareBitmapMaxFdCount implements Experiment {
@@ -583,6 +584,11 @@ static final class ManualOverrideHardwareBitmapMaxFdCount implements Experiment
583584
}
584585
}
585586

587+
/** See {@link #setWaitForFramesAfterTrimMemory(boolean)}. */
588+
public static final class WaitForFramesAfterTrimMemory implements Experiment {
589+
private WaitForFramesAfterTrimMemory() {}
590+
}
591+
586592
static final class EnableImageDecoderForBitmaps implements Experiment {}
587593

588594
/** See {@link #setLogRequestOrigins(boolean)}. */

‎library/src/main/java/com/bumptech/glide/load/resource/bitmap/HardwareConfigState.java

+70-14
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,25 @@
77
import android.util.Log;
88
import androidx.annotation.GuardedBy;
99
import androidx.annotation.VisibleForTesting;
10+
import com.bumptech.glide.util.Util;
1011
import java.io.File;
1112
import java.util.Arrays;
13+
import java.util.concurrent.atomic.AtomicBoolean;
1214

1315
/**
1416
* State and constants for interacting with {@link android.graphics.Bitmap.Config#HARDWARE} on
1517
* Android O+.
1618
*/
1719
public final class HardwareConfigState {
20+
private static final String TAG = "HardwareConfig";
21+
1822
/**
1923
* Force the state to wait until a call to allow hardware Bitmaps to be used when they'd otherwise
2024
* be eligible to work around a framework issue pre Q that can cause a native crash when
2125
* allocating a hardware Bitmap in this specific circumstance. See b/126573603#comment12 for
2226
* details.
2327
*/
24-
private static final boolean BLOCK_HARDWARE_BITMAPS_BY_DEFAULT =
28+
private static final boolean BLOCK_HARDWARE_BITMAPS_WHEN_GL_CONTEXT_MIGHT_NOT_BE_INITIALIZED =
2529
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q;
2630

2731
/**
@@ -87,7 +91,13 @@ public final class HardwareConfigState {
8791
@GuardedBy("this")
8892
private boolean isFdSizeBelowHardwareLimit = true;
8993

90-
private volatile boolean areHardwareBitmapsUnblocked;
94+
/**
95+
* Only mutated on the main thread. Read by any number of background threads concurrently.
96+
*
97+
* <p>Defaults to {@code false} because we need to wait for the GL context to be initialized and
98+
* it defaults to not initialized (https://b.corp.google.com/issues/126573603#comment12).
99+
*/
100+
private final AtomicBoolean isHardwareConfigAllowedByAppState = new AtomicBoolean(false);
91101

92102
public static HardwareConfigState getInstance() {
93103
if (instance == null) {
@@ -112,31 +122,77 @@ public static HardwareConfigState getInstance() {
112122
}
113123
}
114124

115-
public void unblockHardwareBitmaps() {
116-
areHardwareBitmapsUnblocked = true;
125+
public synchronized void blockHardwareBitmaps() {
126+
Util.assertMainThread();
127+
isHardwareConfigAllowedByAppState.set(false);
128+
}
129+
130+
public synchronized void unblockHardwareBitmaps() {
131+
Util.assertMainThread();
132+
isHardwareConfigAllowedByAppState.set(true);
117133
}
118134

119135
public boolean isHardwareConfigAllowed(
120136
int targetWidth,
121137
int targetHeight,
122138
boolean isHardwareConfigAllowed,
123139
boolean isExifOrientationRequired) {
124-
if (!isHardwareConfigAllowed
125-
|| !isHardwareConfigAllowedByDeviceModel
126-
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.O
127-
|| areHardwareBitmapsBlockedByAppState()
128-
|| isExifOrientationRequired) {
140+
if (!isHardwareConfigAllowed) {
141+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
142+
Log.v(TAG, "Hardware config disallowed by caller");
143+
}
144+
return false;
145+
}
146+
if (!isHardwareConfigAllowedByDeviceModel) {
147+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
148+
Log.v(TAG, "Hardware config disallowed by device model");
149+
}
150+
return false;
151+
}
152+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
153+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
154+
Log.v(TAG, "Hardware config disallowed by sdk");
155+
}
156+
return false;
157+
}
158+
if (areHardwareBitmapsBlockedByAppState()) {
159+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
160+
Log.v(TAG, "Hardware config disallowed by app state");
161+
}
162+
return false;
163+
}
164+
if (isExifOrientationRequired) {
165+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
166+
Log.v(TAG, "Hardware config disallowed because exif orientation is required");
167+
}
168+
return false;
169+
}
170+
if (targetWidth < minHardwareDimension) {
171+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
172+
Log.v(TAG, "Hardware config disallowed because width is too small");
173+
}
174+
return false;
175+
}
176+
if (targetHeight < minHardwareDimension) {
177+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
178+
Log.v(TAG, "Hardware config disallowed because height is too small");
179+
}
180+
return false;
181+
}
182+
// Make sure to call isFdSizeBelowHardwareLimit last because it has side affects.
183+
if (!isFdSizeBelowHardwareLimit()) {
184+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
185+
Log.v(TAG, "Hardware config disallowed because there are insufficient FDs");
186+
}
129187
return false;
130188
}
131189

132-
return targetWidth >= minHardwareDimension
133-
&& targetHeight >= minHardwareDimension
134-
// Make sure to call isFdSizeBelowHardwareLimit last because it has side affects.
135-
&& isFdSizeBelowHardwareLimit();
190+
return true;
136191
}
137192

138193
private boolean areHardwareBitmapsBlockedByAppState() {
139-
return BLOCK_HARDWARE_BITMAPS_BY_DEFAULT && !areHardwareBitmapsUnblocked;
194+
return BLOCK_HARDWARE_BITMAPS_WHEN_GL_CONTEXT_MIGHT_NOT_BE_INITIALIZED
195+
&& !isHardwareConfigAllowedByAppState.get();
140196
}
141197

142198
@TargetApi(Build.VERSION_CODES.O)

‎library/src/main/java/com/bumptech/glide/manager/DoNothingFirstFrameWaiter.java

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

33
import android.app.Activity;
44

5-
final class DoNothingFirstFrameWaiter implements FirstFrameWaiter {
5+
final class DoNothingFirstFrameWaiter implements FrameWaiter {
66

77
@Override
88
public void registerSelf(Activity activity) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.bumptech.glide.manager;
2+
3+
import android.app.Activity;
4+
import android.content.ComponentCallbacks2;
5+
import android.content.res.Configuration;
6+
import android.os.Build;
7+
import androidx.annotation.NonNull;
8+
import androidx.annotation.RequiresApi;
9+
10+
@RequiresApi(Build.VERSION_CODES.O)
11+
final class FirstFrameAndAfterTrimMemoryWaiter implements FrameWaiter, ComponentCallbacks2 {
12+
13+
@Override
14+
public void registerSelf(Activity activity) {}
15+
16+
@Override
17+
public void onTrimMemory(int level) {}
18+
19+
@Override
20+
public void onConfigurationChanged(@NonNull Configuration newConfig) {}
21+
22+
@Override
23+
public void onLowMemory() {}
24+
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package com.bumptech.glide.manager;
22

33
import android.app.Activity;
4+
import android.os.Build;
5+
import androidx.annotation.RequiresApi;
46

5-
interface FirstFrameWaiter {
6-
void registerSelf(Activity activity);
7+
@RequiresApi(Build.VERSION_CODES.O)
8+
final class FirstFrameWaiter implements FrameWaiter {
9+
10+
@Override
11+
public void registerSelf(Activity activity) {}
712
}

‎library/src/main/java/com/bumptech/glide/manager/FirstFrameWaiterO.java

-12
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.bumptech.glide.manager;
2+
3+
import android.app.Activity;
4+
5+
interface FrameWaiter {
6+
void registerSelf(Activity activity);
7+
}

‎library/src/main/java/com/bumptech/glide/manager/RequestManagerRetriever.java

+20-9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import androidx.fragment.app.FragmentActivity;
2323
import androidx.fragment.app.FragmentManager;
2424
import com.bumptech.glide.Glide;
25+
import com.bumptech.glide.GlideBuilder.WaitForFramesAfterTrimMemory;
26+
import com.bumptech.glide.GlideExperiments;
2527
import com.bumptech.glide.RequestManager;
2628
import com.bumptech.glide.util.Preconditions;
2729
import com.bumptech.glide.util.Util;
@@ -70,14 +72,23 @@ public class RequestManagerRetriever implements Handler.Callback {
7072
// This is really misplaced here, but to put it anywhere else means duplicating all of the
7173
// Fragment/Activity extraction logic that already exists here. It's gross, but less likely to
7274
// break.
73-
private final FirstFrameWaiter firstFrameWaiter =
74-
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
75-
? new FirstFrameWaiterO()
76-
: new DoNothingFirstFrameWaiter();
75+
private final FrameWaiter frameWaiter;
7776

78-
public RequestManagerRetriever(@Nullable RequestManagerFactory factory) {
77+
public RequestManagerRetriever(
78+
@Nullable RequestManagerFactory factory, GlideExperiments experiments) {
7979
this.factory = factory != null ? factory : DEFAULT_FACTORY;
8080
handler = new Handler(Looper.getMainLooper(), this /* Callback */);
81+
82+
frameWaiter = buildFrameWaiter(experiments);
83+
}
84+
85+
private static FrameWaiter buildFrameWaiter(GlideExperiments experiments) {
86+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
87+
return new DoNothingFirstFrameWaiter();
88+
}
89+
return experiments.isEnabled(WaitForFramesAfterTrimMemory.class)
90+
? new FirstFrameAndAfterTrimMemoryWaiter()
91+
: new FirstFrameWaiter();
8192
}
8293

8394
@NonNull
@@ -133,7 +144,7 @@ public RequestManager get(@NonNull FragmentActivity activity) {
133144
return get(activity.getApplicationContext());
134145
} else {
135146
assertNotDestroyed(activity);
136-
firstFrameWaiter.registerSelf(activity);
147+
frameWaiter.registerSelf(activity);
137148
FragmentManager fm = activity.getSupportFragmentManager();
138149
return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
139150
}
@@ -152,7 +163,7 @@ public RequestManager get(@NonNull Fragment fragment) {
152163
// we manage not to register the first frame waiter for a while, the consequences are not
153164
// catastrophic, we'll just use some extra memory.
154165
if (fragment.getActivity() != null) {
155-
firstFrameWaiter.registerSelf(fragment.getActivity());
166+
frameWaiter.registerSelf(fragment.getActivity());
156167
}
157168
FragmentManager fm = fragment.getChildFragmentManager();
158169
return supportFragmentGet(fragment.getContext(), fm, fragment, fragment.isVisible());
@@ -168,7 +179,7 @@ public RequestManager get(@NonNull Activity activity) {
168179
return get((FragmentActivity) activity);
169180
} else {
170181
assertNotDestroyed(activity);
171-
firstFrameWaiter.registerSelf(activity);
182+
frameWaiter.registerSelf(activity);
172183
android.app.FragmentManager fm = activity.getFragmentManager();
173184
return fragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
174185
}
@@ -353,7 +364,7 @@ public RequestManager get(@NonNull android.app.Fragment fragment) {
353364
// we manage not to register the first frame waiter for a while, the consequences are not
354365
// catastrophic, we'll just use some extra memory.
355366
if (fragment.getActivity() != null) {
356-
firstFrameWaiter.registerSelf(fragment.getActivity());
367+
frameWaiter.registerSelf(fragment.getActivity());
357368
}
358369
android.app.FragmentManager fm = fragment.getChildFragmentManager();
359370
return fragmentGet(fragment.getActivity(), fm, fragment, fragment.isVisible());

‎library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/HardwareConfigStateTest.java

+21-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class HardwareConfigStateTest {
1919
@Config(sdk = Build.VERSION_CODES.O)
2020
@Test
2121
public void
22-
setHardwareConfigIfAllowed_withAllowedState_setsInPreferredConfigAndMutable_returnsFalse() {
22+
setHardwareConfigIfAllowed_withAllowedState_setsInPreferredConfigAndMutable_returnsTrue() {
2323
HardwareConfigState state = new HardwareConfigState();
2424
state.unblockHardwareBitmaps();
2525
BitmapFactory.Options options = new BitmapFactory.Options();
@@ -32,10 +32,29 @@ public class HardwareConfigStateTest {
3232
/*isExifOrientationRequired=*/ false);
3333

3434
assertThat(result).isTrue();
35-
assertThat(options.inMutable).isFalse();
3635
assertThat(options.inPreferredConfig).isEqualTo(Bitmap.Config.HARDWARE);
3736
}
3837

38+
@Config(sdk = Build.VERSION_CODES.O)
39+
@Test
40+
public void
41+
setHardwareConfigIfAllowed_withAllowedState_afterReblock_returnsFalseAndDoesNotSetValues() {
42+
HardwareConfigState state = new HardwareConfigState();
43+
state.unblockHardwareBitmaps();
44+
state.blockHardwareBitmaps();
45+
BitmapFactory.Options options = new BitmapFactory.Options();
46+
boolean result =
47+
state.setHardwareConfigIfAllowed(
48+
/*targetWidth=*/ HardwareConfigState.MIN_HARDWARE_DIMENSION_O,
49+
/*targetHeight=*/ HardwareConfigState.MIN_HARDWARE_DIMENSION_O,
50+
options,
51+
/*isHardwareConfigAllowed=*/ true,
52+
/*isExifOrientationRequired=*/ false);
53+
54+
assertThat(result).isFalse();
55+
assertThat(options.inPreferredConfig).isNotEqualTo(Bitmap.Config.HARDWARE);
56+
}
57+
3958
@Config(sdk = Build.VERSION_CODES.O)
4059
@Test
4160
public void setHardwareConfigIfAllowed_withSmallerThanMinWidth_returnsFalse_doesNotSetValues() {

‎library/test/src/test/java/com/bumptech/glide/manager/RequestManagerRetrieverTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import androidx.fragment.app.FragmentController;
2525
import androidx.fragment.app.FragmentHostCallback;
2626
import androidx.test.core.app.ApplicationProvider;
27+
import com.bumptech.glide.GlideExperiments;
2728
import com.bumptech.glide.RequestManager;
2829
import com.bumptech.glide.tests.BackgroundUtil.BackgroundTester;
2930
import com.bumptech.glide.tests.GlideShadowLooper;
@@ -58,7 +59,7 @@ public class RequestManagerRetrieverTest {
5859
public void setUp() {
5960
appContext = ApplicationProvider.getApplicationContext();
6061

61-
retriever = new RequestManagerRetriever(/*factory=*/ null);
62+
retriever = new RequestManagerRetriever(/*factory=*/ null, mock(GlideExperiments.class));
6263

6364
harnesses =
6465
new RetrieverHarness[] {new DefaultRetrieverHarness(), new SupportRetrieverHarness()};

0 commit comments

Comments
 (0)
Please sign in to comment.