Skip to content

Commit cf1fc01

Browse files
Abbondanzometa-codesync[bot]
authored andcommitted
Move multi-source image disk cache checks off the UI thread (#58701)
Summary: Pull Request resolved: #58701 Avoid synchronous disk I/O while Android selects a source for multi-source images. The best source and bitmap-memory-cache preview are selected immediately. More precise alternate sources are ordered by size precision and submitted as a first-available disk-cache-only intermediate request, so Fresco probes them off the UI thread alongside the primary request while preserving the immediate memory preview. Both Android image-view implementations submit one controller/show request immediately without asynchronous view callbacks or duplicate lifecycles. Drawee snapshots controller instrumentation once per request before its fallback suppliers run. The behavior remains behind a disabled-by-default runtime gate. Changelog: [Android][Fixed] - Avoid blocking the UI thread while selecting multi-source images Reviewed By: javache Differential Revision: D122082126 fbshipit-source-id: 673ecc856f00e9165bf0199c4f4df9e1f29765f4
1 parent 82def6e commit cf1fc01

23 files changed

Lines changed: 521 additions & 133 deletions

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<759fe2f98c26a288f8cd281027aad237>>
7+
* @generated SignedSource<<a7ee8533698805217e86f58648598578>>
88
*/
99

1010
/**
@@ -120,6 +120,12 @@ public object ReactNativeFeatureFlags {
120120
@JvmStatic
121121
public fun enableAndroidTextMeasurementOptimizations(): Boolean = accessor.enableAndroidTextMeasurementOptimizations()
122122

123+
/**
124+
* Runs multi-source image disk-cache checks asynchronously on Android.
125+
*/
126+
@JvmStatic
127+
public fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean = accessor.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
128+
123129
/**
124130
* Feature flag to enable the new bridgeless architecture.
125131
*/

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<1f2c6af3ce83f8f56e6fef3eece891bd>>
7+
* @generated SignedSource<<76f45f25d630cf505f8628e125ab8904>>
88
*/
99

1010
/**
@@ -35,6 +35,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
3535
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
3636
private var enableAndroidAutoOffscreenCompositingForElevationCache: Boolean? = null
3737
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
38+
private var enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache: Boolean? = null
3839
private var enableBridgelessArchitectureCache: Boolean? = null
3940
private var enableCppPropsIteratorSetterCache: Boolean? = null
4041
private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null
@@ -245,6 +246,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
245246
return cached
246247
}
247248

249+
override fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean {
250+
var cached = enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache
251+
if (cached == null) {
252+
cached = ReactNativeFeatureFlagsCxxInterop.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
253+
enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache = cached
254+
}
255+
return cached
256+
}
257+
248258
override fun enableBridgelessArchitecture(): Boolean {
249259
var cached = enableBridgelessArchitectureCache
250260
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<9fe9b6af3dd1aba706dd87a8dd0a9c57>>
7+
* @generated SignedSource<<65ff63eb491771b462831024fafe8078>>
88
*/
99

1010
/**
@@ -58,6 +58,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
5858

5959
@DoNotStrip @JvmStatic public external fun enableAndroidTextMeasurementOptimizations(): Boolean
6060

61+
@DoNotStrip @JvmStatic public external fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean
62+
6163
@DoNotStrip @JvmStatic public external fun enableBridgelessArchitecture(): Boolean
6264

6365
@DoNotStrip @JvmStatic public external fun enableCppPropsIteratorSetter(): Boolean

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<1e15400f6b519fc8e9cd3e576501e7ed>>
7+
* @generated SignedSource<<e3f3716e403f03fc67e73fc6db6904ac>>
88
*/
99

1010
/**
@@ -53,6 +53,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
5353

5454
override fun enableAndroidTextMeasurementOptimizations(): Boolean = false
5555

56+
override fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean = false
57+
5658
override fun enableBridgelessArchitecture(): Boolean = true
5759

5860
override fun enableCppPropsIteratorSetter(): Boolean = false

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<5f805273ebd792ecec1c284ca074e190>>
7+
* @generated SignedSource<<c6b4968eaf615dddef9fed75960f44aa>>
88
*/
99

1010
/**
@@ -39,6 +39,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
3939
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
4040
private var enableAndroidAutoOffscreenCompositingForElevationCache: Boolean? = null
4141
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
42+
private var enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache: Boolean? = null
4243
private var enableBridgelessArchitectureCache: Boolean? = null
4344
private var enableCppPropsIteratorSetterCache: Boolean? = null
4445
private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null
@@ -264,6 +265,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
264265
return cached
265266
}
266267

268+
override fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean {
269+
var cached = enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache
270+
if (cached == null) {
271+
cached = currentProvider.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
272+
accessedFeatureFlags.add("enableAsyncDiskCacheCheckInMultiSourceImageAndroid")
273+
enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache = cached
274+
}
275+
return cached
276+
}
277+
267278
override fun enableBridgelessArchitecture(): Boolean {
268279
var cached = enableBridgelessArchitectureCache
269280
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<c2c4ed4f4bca0b82aefc82dfb43617b7>>
7+
* @generated SignedSource<<62d4a09fa162c10b27df8143e905375b>>
88
*/
99

1010
/**
@@ -53,6 +53,8 @@ public interface ReactNativeFeatureFlagsProvider {
5353

5454
@DoNotStrip public fun enableAndroidTextMeasurementOptimizations(): Boolean
5555

56+
@DoNotStrip public fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean
57+
5658
@DoNotStrip public fun enableBridgelessArchitecture(): Boolean
5759

5860
@DoNotStrip public fun enableCppPropsIteratorSetter(): Boolean

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt‎

Lines changed: 119 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@ import android.graphics.Shader.TileMode
2222
import android.graphics.drawable.Animatable
2323
import android.graphics.drawable.Drawable
2424
import android.net.Uri
25+
import com.facebook.common.internal.Supplier
2526
import com.facebook.common.references.CloseableReference
2627
import com.facebook.common.util.UriUtil
28+
import com.facebook.datasource.DataSource
29+
import com.facebook.datasource.FirstAvailableDataSourceSupplier
30+
import com.facebook.datasource.IncreasingQualityDataSourceSupplier
2731
import com.facebook.drawee.backends.pipeline.Fresco
32+
import com.facebook.drawee.backends.pipeline.PipelineDraweeController
2833
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder
2934
import com.facebook.drawee.controller.ControllerListener
3035
import com.facebook.drawee.controller.ForwardingControllerListener
@@ -35,6 +40,7 @@ import com.facebook.drawee.generic.RoundingParams
3540
import com.facebook.drawee.view.GenericDraweeView
3641
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory
3742
import com.facebook.imagepipeline.common.ResizeOptions
43+
import com.facebook.imagepipeline.common.RotationOptions
3844
import com.facebook.imagepipeline.core.DownsampleMode
3945
import com.facebook.imagepipeline.image.CloseableImage
4046
import com.facebook.imagepipeline.image.ImageInfo
@@ -50,6 +56,7 @@ import com.facebook.react.bridge.ReadableMap
5056
import com.facebook.react.common.annotations.UnstableReactNativeAPI
5157
import com.facebook.react.common.annotations.VisibleForTesting
5258
import com.facebook.react.common.build.ReactBuildConfig
59+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
5360
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
5461
import com.facebook.react.modules.fresco.ImageCacheControl
5562
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
@@ -91,6 +98,7 @@ public class ReactImageView(
9198
private val sources: MutableList<ImageSource> = mutableListOf()
9299
internal var imageSource: ImageSource? = null
93100
private var cachedImageSource: ImageSource? = null
101+
private var diskCacheProbeCandidates: List<ImageSource> = emptyList()
94102
private var defaultImageDrawable: Drawable? = null
95103
private var loadingImageDrawable: Drawable? = null
96104
private var overlayColor = 0
@@ -396,6 +404,10 @@ public class ReactImageView(
396404
}
397405

398406
setSourceImage()
407+
updateViewForSelectedSource()
408+
}
409+
410+
private fun updateViewForSelectedSource() {
399411
val imageSourceSafe = this.imageSource ?: return
400412
val doResize = shouldResize(imageSourceSafe)
401413

@@ -458,16 +470,16 @@ public class ReactImageView(
458470

459471
val resizeOptions = if (doResize) resizeOptions else null
460472

473+
val imagePipeline = Fresco.getImagePipeline()
461474
if (cacheControl == ImageCacheControl.RELOAD) {
462-
val imagePipeline = Fresco.getImagePipeline()
463475
imagePipeline.evictFromCache(uri)
464476
}
465477

466478
val imageRequestBuilder =
467479
ImageRequestBuilder.newBuilderWithSource(uri)
468480
.setPostprocessor(postprocessor)
469481
.setResizeOptions(resizeOptions)
470-
.setAutoRotateEnabled(true)
482+
.setRotationOptions(RotationOptions.autoRotate())
471483
.setProgressiveRenderingEnabled(progressiveRenderingEnabled)
472484
.setLowestPermittedRequestLevel(requestLevel)
473485

@@ -493,24 +505,101 @@ public class ReactImageView(
493505

494506
// This builder is reused
495507
builder.reset()
508+
builder.setDataSourceSupplier(null)
509+
510+
builder.setAutoPlayAnimations(true).setOldController(controller)
511+
512+
val cachedImageRequest =
513+
cachedImageSource?.let { cachedSource ->
514+
val cachedImageRequestBuilder =
515+
ImageRequestBuilder.newBuilderWithSource(cachedSource.uri)
516+
.setPostprocessor(postprocessor)
517+
.setResizeOptions(resizeOptions)
518+
.setRotationOptions(RotationOptions.autoRotate())
519+
.setProgressiveRenderingEnabled(progressiveRenderingEnabled)
520+
if (resizeMethod == ImageResizeMethod.NONE) {
521+
cachedImageRequestBuilder.setDownsampleOverride(DownsampleMode.NEVER)
522+
}
523+
cachedImageRequestBuilder.build()
524+
}
496525

497-
builder.setImageRequest(imageRequest).setAutoPlayAnimations(true).setOldController(controller)
498-
499-
callerContext?.let { builder.setCallerContext(it) }
500-
501-
cachedImageSource?.let { cachedSource ->
502-
val cachedImageRequestBuilder =
503-
ImageRequestBuilder.newBuilderWithSource(cachedSource.uri)
504-
.setPostprocessor(postprocessor)
505-
.setResizeOptions(resizeOptions)
506-
.setAutoRotateEnabled(true)
507-
.setProgressiveRenderingEnabled(progressiveRenderingEnabled)
508-
if (resizeMethod == ImageResizeMethod.NONE) {
509-
cachedImageRequestBuilder.setDownsampleOverride(DownsampleMode.NEVER)
526+
if (diskCacheProbeCandidates.isEmpty()) {
527+
builder.setImageRequest(imageRequest).setLowResImageRequest(cachedImageRequest)
528+
} else {
529+
val diskCacheRequests = ArrayList<ImageRequest>(diskCacheProbeCandidates.size)
530+
for (source in diskCacheProbeCandidates) {
531+
val diskCacheRequestBuilder =
532+
ImageRequestBuilder.newBuilderWithSource(source.uri)
533+
.setPostprocessor(postprocessor)
534+
.setResizeOptions(resizeOptions)
535+
.setRotationOptions(RotationOptions.autoRotate())
536+
.setProgressiveRenderingEnabled(progressiveRenderingEnabled)
537+
.setLowestPermittedRequestLevel(RequestLevel.DISK_CACHE)
538+
if (resizeMethod == ImageResizeMethod.NONE) {
539+
diskCacheRequestBuilder.setDownsampleOverride(DownsampleMode.NEVER)
540+
}
541+
diskCacheRequests.add(
542+
ReactNetworkImageRequest.fromBuilderWithHeaders(
543+
diskCacheRequestBuilder,
544+
headers,
545+
source.cacheControl,
546+
),
547+
)
510548
}
511-
builder.setLowResImageRequest(cachedImageRequestBuilder.build())
549+
val requestCallerContext = callerContext
550+
builder.setDataSourceSupplier(
551+
Supplier {
552+
val pipelineController = controller as? PipelineDraweeController
553+
val requestListener = pipelineController?.requestListener
554+
val controllerId = pipelineController?.id
555+
fun dataSourceSupplierFor(request: ImageRequest, level: RequestLevel) =
556+
imagePipeline.getDataSourceSupplier(
557+
request,
558+
requestCallerContext,
559+
level,
560+
requestListener,
561+
controllerId,
562+
)
563+
564+
val diskCacheDataSourceSuppliers =
565+
ArrayList<
566+
Supplier<DataSource<CloseableReference<CloseableImage>>>,
567+
>(
568+
diskCacheRequests.size,
569+
)
570+
for (request in diskCacheRequests) {
571+
diskCacheDataSourceSuppliers.add(
572+
dataSourceSupplierFor(request, RequestLevel.FULL_FETCH),
573+
)
574+
}
575+
val diskCacheDataSourceSupplier =
576+
FirstAvailableDataSourceSupplier.create(diskCacheDataSourceSuppliers)
577+
val lowerResDataSourceSupplier =
578+
cachedImageRequest?.let { memoryCacheRequest ->
579+
IncreasingQualityDataSourceSupplier.create(
580+
arrayListOf(
581+
diskCacheDataSourceSupplier,
582+
dataSourceSupplierFor(
583+
memoryCacheRequest,
584+
RequestLevel.BITMAP_MEMORY_CACHE,
585+
),
586+
),
587+
imagePipeline.isLazyDataSource.get(),
588+
)
589+
} ?: diskCacheDataSourceSupplier
590+
val highResDataSourceSupplier =
591+
dataSourceSupplierFor(imageRequest, RequestLevel.FULL_FETCH)
592+
IncreasingQualityDataSourceSupplier.create(
593+
arrayListOf(highResDataSourceSupplier, lowerResDataSourceSupplier),
594+
imagePipeline.isLazyDataSource.get(),
595+
)
596+
.get()
597+
},
598+
)
512599
}
513600

601+
callerContext?.let { builder.setCallerContext(it) }
602+
514603
if (downloadListener != null && controllerForTesting != null) {
515604
val combinedListener: ForwardingControllerListener<ImageInfo> =
516605
ForwardingControllerListener<ImageInfo>()
@@ -531,6 +620,7 @@ public class ReactImageView(
531620

532621
// Reset again so the DraweeControllerBuilder clears all it's references. Otherwise, this causes
533622
// a memory leak.
623+
builder.setDataSourceSupplier(null)
534624
builder.reset()
535625
}
536626

@@ -556,12 +646,24 @@ public class ReactImageView(
556646

557647
private fun setSourceImage() {
558648
imageSource = null
649+
cachedImageSource = null
650+
diskCacheProbeCandidates = emptyList()
559651
if (sources.isEmpty()) {
560652
sources.add(getTransparentBitmapImageSource(context))
561653
} else if (hasMultipleSources()) {
562-
val multiSource = getBestSourceForSize(width, height, sources)
654+
val asyncDiskCacheCheckEnabled =
655+
ReactNativeFeatureFlags.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
656+
val multiSource =
657+
getBestSourceForSize(
658+
width,
659+
height,
660+
sources,
661+
1.0,
662+
checkDiskCache = !asyncDiskCacheCheckEnabled,
663+
)
563664
imageSource = multiSource.bestResult
564665
cachedImageSource = multiSource.bestResultInCache
666+
diskCacheProbeCandidates = multiSource.diskCacheProbeCandidates
565667
return
566668
}
567669
imageSource = sources[0]

0 commit comments

Comments
 (0)