Skip to content

Commit 12397f6

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Move multi-source image disk cache checks off the UI thread (#58701)
Summary: 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
1 parent 99aefad commit 12397f6

23 files changed

Lines changed: 522 additions & 134 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<<86390dbd6b2c57cb4f8cdd7cb73cc1f7>>
7+
* @generated SignedSource<<5903d783a8d1544f64c0b6b0497ce3c9>>
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<<3e939eebe846d12cab15905db8efba18>>
7+
* @generated SignedSource<<62f32d29de0188f7fc4fb8610c0f7243>>
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 enableBufferedCallInvokerCache: Boolean? = null
4041
private var enableCppPropsIteratorSetterCache: Boolean? = null
@@ -246,6 +247,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
246247
return cached
247248
}
248249

250+
override fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean {
251+
var cached = enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache
252+
if (cached == null) {
253+
cached = ReactNativeFeatureFlagsCxxInterop.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
254+
enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache = cached
255+
}
256+
return cached
257+
}
258+
249259
override fun enableBridgelessArchitecture(): Boolean {
250260
var cached = enableBridgelessArchitectureCache
251261
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<<da34d40665437b2901de2ca5f0bee7c1>>
7+
* @generated SignedSource<<ff20dd706d265e8e21c224ba1bb65668>>
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 enableBufferedCallInvoker(): 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<<0f4c074c11c564f05950eab73a8fd452>>
7+
* @generated SignedSource<<fe7eec55afb3b82e91491a2bfdd0dd45>>
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 enableBufferedCallInvoker(): Boolean = true

‎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<<013790bd2c3fa8b9ff03e8c5e924bf91>>
7+
* @generated SignedSource<<0dedeabd0a4104b0498c09bdc84f0e4f>>
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 enableBufferedCallInvokerCache: Boolean? = null
4445
private var enableCppPropsIteratorSetterCache: Boolean? = null
@@ -265,6 +266,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
265266
return cached
266267
}
267268

269+
override fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean {
270+
var cached = enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache
271+
if (cached == null) {
272+
cached = currentProvider.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
273+
accessedFeatureFlags.add("enableAsyncDiskCacheCheckInMultiSourceImageAndroid")
274+
enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache = cached
275+
}
276+
return cached
277+
}
278+
268279
override fun enableBridgelessArchitecture(): Boolean {
269280
var cached = enableBridgelessArchitectureCache
270281
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<<6607c8678df2693c361f6d8091483662>>
7+
* @generated SignedSource<<9c4c2248532c2a04d150ad7edba28260>>
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 enableBufferedCallInvoker(): 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)