Skip to content

Commit db58a7e

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 candidate are selected immediately, then Fresco's asynchronous disk-cache API finds the most precise cached source and upgrades the request when it completes. Both Android image-view implementations reject stale callbacks after a new request, source update, size change, or detach. 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 cd54f36 commit db58a7e

23 files changed

Lines changed: 529 additions & 116 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<<ab68be1fe69275aae7e13121d0827f0e>>
7+
* @generated SignedSource<<f9148ea52fcf53102b5c95f1dcfb8d50>>
88
*/
99

1010
/**
@@ -114,6 +114,12 @@ public object ReactNativeFeatureFlags {
114114
@JvmStatic
115115
public fun enableAndroidTextMeasurementOptimizations(): Boolean = accessor.enableAndroidTextMeasurementOptimizations()
116116

117+
/**
118+
* Runs multi-source image disk-cache checks asynchronously on Android.
119+
*/
120+
@JvmStatic
121+
public fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean = accessor.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
122+
117123
/**
118124
* Feature flag to enable the new bridgeless architecture.
119125
*/

‎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<<00e1e0529cbef727ab81a6a75174c87e>>
7+
* @generated SignedSource<<e5f26281df4716ac5e2b88403052c747>>
88
*/
99

1010
/**
@@ -34,6 +34,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
3434
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
3535
private var enableAndroidAutoOffscreenCompositingForElevationCache: Boolean? = null
3636
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
37+
private var enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache: Boolean? = null
3738
private var enableBridgelessArchitectureCache: Boolean? = null
3839
private var enableBufferedCallInvokerCache: Boolean? = null
3940
private var enableCppPropsIteratorSetterCache: Boolean? = null
@@ -235,6 +236,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
235236
return cached
236237
}
237238

239+
override fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean {
240+
var cached = enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache
241+
if (cached == null) {
242+
cached = ReactNativeFeatureFlagsCxxInterop.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
243+
enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache = cached
244+
}
245+
return cached
246+
}
247+
238248
override fun enableBridgelessArchitecture(): Boolean {
239249
var cached = enableBridgelessArchitectureCache
240250
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<<427c98d364e8a0aec3a0834b6a8117c8>>
7+
* @generated SignedSource<<ac82fd72cba29452d1604770ab19804f>>
88
*/
99

1010
/**
@@ -56,6 +56,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
5656

5757
@DoNotStrip @JvmStatic public external fun enableAndroidTextMeasurementOptimizations(): Boolean
5858

59+
@DoNotStrip @JvmStatic public external fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean
60+
5961
@DoNotStrip @JvmStatic public external fun enableBridgelessArchitecture(): Boolean
6062

6163
@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<<b6c47b10d221b493df4038e492482961>>
7+
* @generated SignedSource<<80a561079091b74aa3c682296ffe7da3>>
88
*/
99

1010
/**
@@ -51,6 +51,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
5151

5252
override fun enableAndroidTextMeasurementOptimizations(): Boolean = false
5353

54+
override fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean = false
55+
5456
override fun enableBridgelessArchitecture(): Boolean = true
5557

5658
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<<20a908f1ef475bb5f51b631e2d0f2038>>
7+
* @generated SignedSource<<3dd1a8c6227c3a1186ceaad7e3f0fc24>>
88
*/
99

1010
/**
@@ -38,6 +38,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
3838
private var enableAccumulatedUpdatesInRawPropsAndroidCache: Boolean? = null
3939
private var enableAndroidAutoOffscreenCompositingForElevationCache: Boolean? = null
4040
private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null
41+
private var enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache: Boolean? = null
4142
private var enableBridgelessArchitectureCache: Boolean? = null
4243
private var enableBufferedCallInvokerCache: Boolean? = null
4344
private var enableCppPropsIteratorSetterCache: Boolean? = null
@@ -253,6 +254,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
253254
return cached
254255
}
255256

257+
override fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean {
258+
var cached = enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache
259+
if (cached == null) {
260+
cached = currentProvider.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
261+
accessedFeatureFlags.add("enableAsyncDiskCacheCheckInMultiSourceImageAndroid")
262+
enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache = cached
263+
}
264+
return cached
265+
}
266+
256267
override fun enableBridgelessArchitecture(): Boolean {
257268
var cached = enableBridgelessArchitectureCache
258269
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<<ac56780559e180d552086f3c53a1f111>>
7+
* @generated SignedSource<<7c81e9f6b6859f3bdc1ea8ad5539be5b>>
88
*/
99

1010
/**
@@ -51,6 +51,8 @@ public interface ReactNativeFeatureFlagsProvider {
5151

5252
@DoNotStrip public fun enableAndroidTextMeasurementOptimizations(): Boolean
5353

54+
@DoNotStrip public fun enableAsyncDiskCacheCheckInMultiSourceImageAndroid(): Boolean
55+
5456
@DoNotStrip public fun enableBridgelessArchitecture(): Boolean
5557

5658
@DoNotStrip public fun enableBufferedCallInvoker(): Boolean

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ import com.facebook.imagepipeline.request.Postprocessor
4747
import com.facebook.react.bridge.ReactContext
4848
import com.facebook.react.bridge.ReadableArray
4949
import com.facebook.react.bridge.ReadableMap
50+
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
5051
import com.facebook.react.common.annotations.UnstableReactNativeAPI
5152
import com.facebook.react.common.annotations.VisibleForTesting
5253
import com.facebook.react.common.build.ReactBuildConfig
54+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
5355
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags
5456
import com.facebook.react.modules.fresco.ImageCacheControl
5557
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
@@ -72,6 +74,7 @@ import com.facebook.react.views.image.ImageResizeMode.defaultValue
7274
import com.facebook.react.views.image.MultiPostprocessor.Companion.from
7375
import com.facebook.react.views.imagehelper.ImageSource
7476
import com.facebook.react.views.imagehelper.ImageSource.Companion.getTransparentBitmapImageSource
77+
import com.facebook.react.views.imagehelper.MultiSourceHelper
7578
import com.facebook.react.views.imagehelper.MultiSourceHelper.getBestSourceForSize
7679
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
7780
import kotlin.math.abs
@@ -106,6 +109,7 @@ public class ReactImageView(
106109
private var headers: ReadableMap? = null
107110
private var resizeMultiplier = 1.0f
108111
private var resizeMethod = ImageResizeMethod.AUTO
112+
private var sourceGeneration = 0
109113

110114
init {
111115
// Workaround Android bug where ImageView visibility is not propagated to the Drawable, so you
@@ -313,6 +317,7 @@ public class ReactImageView(
313317

314318
this.sources.clear()
315319
this.sources.addAll(tmpSources)
320+
sourceGeneration++
316321
isDirty = true
317322
}
318323

@@ -544,6 +549,7 @@ public class ReactImageView(
544549
protected override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
545550
super.onSizeChanged(w, h, oldw, oldh)
546551
if (w > 0 && h > 0) {
552+
sourceGeneration++
547553
isDirty = isDirty || hasMultipleSources() || isTiled
548554
maybeUpdateView()
549555
}
@@ -555,13 +561,38 @@ public class ReactImageView(
555561
get() = tileMode != TileMode.CLAMP
556562

557563
private fun setSourceImage() {
564+
sourceGeneration++
558565
imageSource = null
566+
cachedImageSource = null
559567
if (sources.isEmpty()) {
560568
sources.add(getTransparentBitmapImageSource(context))
561569
} else if (hasMultipleSources()) {
562570
val multiSource = getBestSourceForSize(width, height, sources)
563571
imageSource = multiSource.bestResult
564572
cachedImageSource = multiSource.bestResultInCache
573+
val selectedSource = imageSource
574+
if (
575+
selectedSource != null &&
576+
selectedSource.cacheControl != ImageCacheControl.RELOAD &&
577+
ReactNativeFeatureFlags.enableAsyncDiskCacheCheckInMultiSourceImageAndroid()
578+
) {
579+
val generation = sourceGeneration
580+
MultiSourceHelper.findBestCachedSourceFromDiskAsync(width, height, sources, 1.0) { result ->
581+
runOnUiThread {
582+
if (
583+
generation != sourceGeneration ||
584+
!isAttachedToWindow ||
585+
result == null ||
586+
result.source == cachedImageSource?.source ||
587+
imageSource?.source != selectedSource.source
588+
) {
589+
return@runOnUiThread
590+
}
591+
cachedImageSource = result
592+
maybeUpdateViewFromRequest(shouldResize(selectedSource))
593+
}
594+
}
595+
}
565596
return
566597
}
567598
imageSource = sources[0]

0 commit comments

Comments
 (0)