Skip to content

Commit 8ad2401

Browse files
Abbondanzofacebook-github-bot
authored andcommitted
Move multi-source image disk cache checks off the UI thread
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 Differential Revision: D122082126
1 parent 51e9a97 commit 8ad2401

23 files changed

Lines changed: 507 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: 111 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ 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.FirstAvailableDataSourceSupplier
29+
import com.facebook.datasource.IncreasingQualityDataSourceSupplier
2730
import com.facebook.drawee.backends.pipeline.Fresco
31+
import com.facebook.drawee.backends.pipeline.PipelineDraweeController
2832
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder
2933
import com.facebook.drawee.controller.ControllerListener
3034
import com.facebook.drawee.controller.ForwardingControllerListener
@@ -35,6 +39,7 @@ import com.facebook.drawee.generic.RoundingParams
3539
import com.facebook.drawee.view.GenericDraweeView
3640
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory
3741
import com.facebook.imagepipeline.common.ResizeOptions
42+
import com.facebook.imagepipeline.common.RotationOptions
3843
import com.facebook.imagepipeline.core.DownsampleMode
3944
import com.facebook.imagepipeline.image.CloseableImage
4045
import com.facebook.imagepipeline.image.ImageInfo
@@ -50,6 +55,7 @@ import com.facebook.react.bridge.ReadableMap
5055
import com.facebook.react.common.annotations.UnstableReactNativeAPI
5156
import com.facebook.react.common.annotations.VisibleForTesting
5257
import com.facebook.react.common.build.ReactBuildConfig
58+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
5359
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
5460
import com.facebook.react.modules.fresco.ImageCacheControl
5561
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
@@ -91,6 +97,7 @@ public class ReactImageView(
9197
private val sources: MutableList<ImageSource> = mutableListOf()
9298
internal var imageSource: ImageSource? = null
9399
private var cachedImageSource: ImageSource? = null
100+
private var diskCacheCandidates: List<ImageSource> = emptyList()
94101
private var defaultImageDrawable: Drawable? = null
95102
private var loadingImageDrawable: Drawable? = null
96103
private var overlayColor = 0
@@ -396,6 +403,10 @@ public class ReactImageView(
396403
}
397404

398405
setSourceImage()
406+
updateViewForSelectedSource()
407+
}
408+
409+
private fun updateViewForSelectedSource() {
399410
val imageSourceSafe = this.imageSource ?: return
400411
val doResize = shouldResize(imageSourceSafe)
401412

@@ -458,16 +469,16 @@ public class ReactImageView(
458469

459470
val resizeOptions = if (doResize) resizeOptions else null
460471

472+
val imagePipeline = Fresco.getImagePipeline()
461473
if (cacheControl == ImageCacheControl.RELOAD) {
462-
val imagePipeline = Fresco.getImagePipeline()
463474
imagePipeline.evictFromCache(uri)
464475
}
465476

466477
val imageRequestBuilder =
467478
ImageRequestBuilder.newBuilderWithSource(uri)
468479
.setPostprocessor(postprocessor)
469480
.setResizeOptions(resizeOptions)
470-
.setAutoRotateEnabled(true)
481+
.setRotationOptions(RotationOptions.autoRotate())
471482
.setProgressiveRenderingEnabled(progressiveRenderingEnabled)
472483
.setLowestPermittedRequestLevel(requestLevel)
473484

@@ -493,24 +504,94 @@ public class ReactImageView(
493504

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

497-
builder.setImageRequest(imageRequest).setAutoPlayAnimations(true).setOldController(controller)
498-
499-
callerContext?.let { builder.setCallerContext(it) }
525+
if (diskCacheCandidates.isEmpty()) {
526+
builder.setImageRequest(imageRequest).setLowResImageRequest(cachedImageRequest)
527+
} else {
528+
val diskCacheRequests =
529+
diskCacheCandidates.map { source ->
530+
val diskCacheRequestBuilder =
531+
ImageRequestBuilder.newBuilderWithSource(source.uri)
532+
.setPostprocessor(postprocessor)
533+
.setResizeOptions(resizeOptions)
534+
.setRotationOptions(RotationOptions.autoRotate())
535+
.setProgressiveRenderingEnabled(progressiveRenderingEnabled)
536+
.setLowestPermittedRequestLevel(RequestLevel.DISK_CACHE)
537+
if (resizeMethod == ImageResizeMethod.NONE) {
538+
diskCacheRequestBuilder.setDownsampleOverride(DownsampleMode.NEVER)
539+
}
540+
val diskCacheRequest =
541+
ReactNetworkImageRequest.fromBuilderWithHeaders(
542+
diskCacheRequestBuilder,
543+
headers,
544+
source.cacheControl,
545+
)
546+
diskCacheRequest
547+
}
548+
val requestCallerContext = callerContext
549+
builder.setDataSourceSupplier(
550+
Supplier {
551+
val pipelineController = controller as? PipelineDraweeController
552+
val requestListener = pipelineController?.requestListener
553+
val controllerId = pipelineController?.id
554+
fun dataSourceSupplierFor(request: ImageRequest, level: RequestLevel) =
555+
imagePipeline.getDataSourceSupplier(
556+
request,
557+
requestCallerContext,
558+
level,
559+
requestListener,
560+
controllerId,
561+
)
500562

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)
510-
}
511-
builder.setLowResImageRequest(cachedImageRequestBuilder.build())
563+
val diskCacheDataSourceSupplier =
564+
FirstAvailableDataSourceSupplier.create(
565+
diskCacheRequests.map { request ->
566+
dataSourceSupplierFor(request, RequestLevel.FULL_FETCH)
567+
},
568+
)
569+
val lowerResDataSourceSupplier =
570+
cachedImageRequest?.let { memoryCacheRequest ->
571+
IncreasingQualityDataSourceSupplier.create(
572+
arrayListOf(
573+
diskCacheDataSourceSupplier,
574+
dataSourceSupplierFor(
575+
memoryCacheRequest,
576+
RequestLevel.BITMAP_MEMORY_CACHE,
577+
),
578+
),
579+
imagePipeline.isLazyDataSource.get(),
580+
)
581+
} ?: diskCacheDataSourceSupplier
582+
val highResDataSourceSupplier =
583+
dataSourceSupplierFor(imageRequest, RequestLevel.FULL_FETCH)
584+
IncreasingQualityDataSourceSupplier.create(
585+
arrayListOf(highResDataSourceSupplier, lowerResDataSourceSupplier),
586+
imagePipeline.isLazyDataSource.get(),
587+
)
588+
.get()
589+
},
590+
)
512591
}
513592

593+
callerContext?.let { builder.setCallerContext(it) }
594+
514595
if (downloadListener != null && controllerForTesting != null) {
515596
val combinedListener: ForwardingControllerListener<ImageInfo> =
516597
ForwardingControllerListener<ImageInfo>()
@@ -531,6 +612,7 @@ public class ReactImageView(
531612

532613
// Reset again so the DraweeControllerBuilder clears all it's references. Otherwise, this causes
533614
// a memory leak.
615+
builder.setDataSourceSupplier(null)
534616
builder.reset()
535617
}
536618

@@ -556,12 +638,24 @@ public class ReactImageView(
556638

557639
private fun setSourceImage() {
558640
imageSource = null
641+
cachedImageSource = null
642+
diskCacheCandidates = emptyList()
559643
if (sources.isEmpty()) {
560644
sources.add(getTransparentBitmapImageSource(context))
561645
} else if (hasMultipleSources()) {
562-
val multiSource = getBestSourceForSize(width, height, sources)
646+
val asyncDiskCacheCheckEnabled =
647+
ReactNativeFeatureFlags.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
648+
val multiSource =
649+
getBestSourceForSize(
650+
width,
651+
height,
652+
sources,
653+
1.0,
654+
checkDiskCache = !asyncDiskCacheCheckEnabled,
655+
)
563656
imageSource = multiSource.bestResult
564657
cachedImageSource = multiSource.bestResultInCache
658+
diskCacheCandidates = multiSource.diskCacheCandidates
565659
return
566660
}
567661
imageSource = sources[0]

0 commit comments

Comments
 (0)