mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
feat(image): enabling basic cache control for android (#47182)
Summary: Fixes https://github.com/facebook/react-native/issues/12606 Previously, `Image` cache control options were not functional on Android, even though they were being passed to the native component via the `source` prop. This PR addresses that by implementing logic to manage cache behaviour on Android. When the `reload` option is explicitly set, the image is now evicted from both memory and disk caches before a new request is made. This ensures the image is always fetched from the source, aligning the caching behaviour between Android and iOS for the `default` and `reload` options. ## Changelog: [ANDROID][ADDED] - Enabling basic `Image` cache control for Android Pull Request resolved: https://github.com/facebook/react-native/pull/47182 Test Plan: Added a new example to the `rn-tester`, where we can notice that the image on the right is reloaded if rendered or re-rendered as the cache policy is set to `reload`. The image on the left has the cache policy set to `default` and won't be re-rendered as the image is already in the cache. See the video below: https://github.com/user-attachments/assets/88bc1d2d-0239-4deb-bcde-fe0ce521ff4d Also tested on both old and new architecture. Reviewed By: NickGerleman Differential Revision: D64915440 Pulled By: Abbondanzo fbshipit-source-id: 32e1c55dd20bf96ab0f69ef900d821c3c2552ef7
This commit is contained in:
committed by
Facebook GitHub Bot
parent
a12036f7be
commit
e5dd7d68bf
+1
-1
@@ -51,7 +51,7 @@ export interface ImageURISource {
|
||||
* to a URL load request, no attempt is made to load the data from the originating source,
|
||||
* and the load is considered to have failed.
|
||||
*
|
||||
* @platform ios
|
||||
* @platform ios (for `force-cache` and `only-if-cached`)
|
||||
*/
|
||||
cache?: 'default' | 'reload' | 'force-cache' | 'only-if-cached' | undefined;
|
||||
/**
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ export interface ImageURISource {
|
||||
* to a URL load request, no attempt is made to load the data from the originating source,
|
||||
* and the load is considered to have failed.
|
||||
*
|
||||
* @platform ios
|
||||
* @platform ios (for `force-cache` and `only-if-cached`)
|
||||
*/
|
||||
+cache?: ?('default' | 'reload' | 'force-cache' | 'only-if-cached');
|
||||
|
||||
|
||||
@@ -3377,14 +3377,25 @@ public final class com/facebook/react/modules/fresco/FrescoModule$Companion {
|
||||
public final fun hasBeenInitialized ()Z
|
||||
}
|
||||
|
||||
public final class com/facebook/react/modules/fresco/ImageCacheControl : java/lang/Enum {
|
||||
public static final field DEFAULT Lcom/facebook/react/modules/fresco/ImageCacheControl;
|
||||
public static final field RELOAD Lcom/facebook/react/modules/fresco/ImageCacheControl;
|
||||
public static fun getEntries ()Lkotlin/enums/EnumEntries;
|
||||
public static fun valueOf (Ljava/lang/String;)Lcom/facebook/react/modules/fresco/ImageCacheControl;
|
||||
public static fun values ()[Lcom/facebook/react/modules/fresco/ImageCacheControl;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/modules/fresco/ReactNetworkImageRequest : com/facebook/imagepipeline/request/ImageRequest {
|
||||
public static final field Companion Lcom/facebook/react/modules/fresco/ReactNetworkImageRequest$Companion;
|
||||
public synthetic fun <init> (Lcom/facebook/imagepipeline/request/ImageRequestBuilder;Lcom/facebook/react/bridge/ReadableMap;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public synthetic fun <init> (Lcom/facebook/imagepipeline/request/ImageRequestBuilder;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/modules/fresco/ImageCacheControl;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public static final fun fromBuilderWithHeaders (Lcom/facebook/imagepipeline/request/ImageRequestBuilder;Lcom/facebook/react/bridge/ReadableMap;)Lcom/facebook/react/modules/fresco/ReactNetworkImageRequest;
|
||||
public static final fun fromBuilderWithHeaders (Lcom/facebook/imagepipeline/request/ImageRequestBuilder;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/modules/fresco/ImageCacheControl;)Lcom/facebook/react/modules/fresco/ReactNetworkImageRequest;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/modules/fresco/ReactNetworkImageRequest$Companion {
|
||||
public final fun fromBuilderWithHeaders (Lcom/facebook/imagepipeline/request/ImageRequestBuilder;Lcom/facebook/react/bridge/ReadableMap;)Lcom/facebook/react/modules/fresco/ReactNetworkImageRequest;
|
||||
public final fun fromBuilderWithHeaders (Lcom/facebook/imagepipeline/request/ImageRequestBuilder;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/modules/fresco/ImageCacheControl;)Lcom/facebook/react/modules/fresco/ReactNetworkImageRequest;
|
||||
public static synthetic fun fromBuilderWithHeaders$default (Lcom/facebook/react/modules/fresco/ReactNetworkImageRequest$Companion;Lcom/facebook/imagepipeline/request/ImageRequestBuilder;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/modules/fresco/ImageCacheControl;ILjava/lang/Object;)Lcom/facebook/react/modules/fresco/ReactNetworkImageRequest;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/modules/fresco/SystraceRequestListener : com/facebook/imagepipeline/listener/BaseRequestListener {
|
||||
@@ -6707,8 +6718,10 @@ public class com/facebook/react/views/imagehelper/ImageSource {
|
||||
public fun <init> (Landroid/content/Context;Ljava/lang/String;)V
|
||||
public fun <init> (Landroid/content/Context;Ljava/lang/String;D)V
|
||||
public fun <init> (Landroid/content/Context;Ljava/lang/String;DD)V
|
||||
public synthetic fun <init> (Landroid/content/Context;Ljava/lang/String;DDILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun <init> (Landroid/content/Context;Ljava/lang/String;DDLcom/facebook/react/modules/fresco/ImageCacheControl;)V
|
||||
public synthetic fun <init> (Landroid/content/Context;Ljava/lang/String;DDLcom/facebook/react/modules/fresco/ImageCacheControl;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getCacheControl ()Lcom/facebook/react/modules/fresco/ImageCacheControl;
|
||||
public final fun getSize ()D
|
||||
public final fun getSource ()Ljava/lang/String;
|
||||
public static final fun getTransparentBitmapImageSource (Landroid/content/Context;)Lcom/facebook/react/views/imagehelper/ImageSource;
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.modules.fresco
|
||||
|
||||
public enum class ImageCacheControl {
|
||||
/** Uses OkHttp's default cache control policy with no store. */
|
||||
DEFAULT,
|
||||
/**
|
||||
* The data for the URL will be loaded from the originating source. No existing cache data should
|
||||
* be used to satisfy a URL load request.
|
||||
*/
|
||||
RELOAD,
|
||||
}
|
||||
+6
-3
@@ -16,16 +16,19 @@ public class ReactNetworkImageRequest
|
||||
private constructor(
|
||||
builder: ImageRequestBuilder,
|
||||
/** Headers for the request */
|
||||
internal val headers: ReadableMap?
|
||||
internal val headers: ReadableMap?,
|
||||
internal val cacheControl: ImageCacheControl,
|
||||
) : ImageRequest(builder) {
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
public fun fromBuilderWithHeaders(
|
||||
builder: ImageRequestBuilder,
|
||||
headers: ReadableMap?
|
||||
headers: ReadableMap?,
|
||||
cacheControl: ImageCacheControl = ImageCacheControl.DEFAULT,
|
||||
): ReactNetworkImageRequest {
|
||||
return ReactNetworkImageRequest(builder, headers)
|
||||
return ReactNetworkImageRequest(builder, headers, cacheControl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -35,22 +35,27 @@ internal class ReactOkHttpNetworkFetcher(private val okHttpClient: OkHttpClient)
|
||||
fetchState.submitTime = SystemClock.elapsedRealtime()
|
||||
val uri = fetchState.uri
|
||||
var requestHeaders: Map<String, String>? = null
|
||||
val cacheControlBuilder = CacheControl.Builder().noStore()
|
||||
if (fetchState.context.imageRequest is ReactNetworkImageRequest) {
|
||||
val networkImageRequest = fetchState.context.imageRequest as ReactNetworkImageRequest
|
||||
requestHeaders = getHeaders(networkImageRequest.headers)
|
||||
when (networkImageRequest.cacheControl) {
|
||||
ImageCacheControl.RELOAD -> {
|
||||
cacheControlBuilder.noCache()
|
||||
}
|
||||
ImageCacheControl.DEFAULT -> {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
}
|
||||
val headers = OkHttpCompat.getHeadersFromMap(requestHeaders)
|
||||
val request =
|
||||
Request.Builder()
|
||||
.cacheControl(CacheControl.Builder().noStore().build())
|
||||
.cacheControl(cacheControlBuilder.build())
|
||||
.url(uri.toString())
|
||||
.headers(headers)
|
||||
.get()
|
||||
.build()
|
||||
fetchWithRequest(fetchState, callback, request)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "ReactOkHttpNetworkFetcher"
|
||||
}
|
||||
}
|
||||
|
||||
+25
-4
@@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import com.facebook.common.references.CloseableReference
|
||||
import com.facebook.common.util.UriUtil
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder
|
||||
import com.facebook.drawee.controller.ControllerListener
|
||||
import com.facebook.drawee.controller.ForwardingControllerListener
|
||||
@@ -49,6 +50,7 @@ import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
||||
import com.facebook.react.common.annotations.VisibleForTesting
|
||||
import com.facebook.react.common.build.ReactBuildConfig
|
||||
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
|
||||
import com.facebook.react.modules.fresco.ImageCacheControl
|
||||
import com.facebook.react.modules.fresco.ReactNetworkImageRequest
|
||||
import com.facebook.react.uimanager.BackgroundStyleApplicator
|
||||
import com.facebook.react.uimanager.LengthPercentage
|
||||
@@ -268,7 +270,8 @@ public class ReactImageView(
|
||||
} else if (sources.size() == 1) {
|
||||
// Optimize for the case where we have just one uri, case in which we don't need the sizes
|
||||
val source = sources.getMap(0)
|
||||
var imageSource = ImageSource(context, source.getString("uri"))
|
||||
val cacheControl = computeCacheControl(source.getString("cache"))
|
||||
var imageSource = ImageSource(context, source.getString("uri"), cacheControl = cacheControl)
|
||||
if (Uri.EMPTY == imageSource.uri) {
|
||||
warnImageSource(source.getString("uri"))
|
||||
imageSource = getTransparentBitmapImageSource(context)
|
||||
@@ -277,12 +280,14 @@ public class ReactImageView(
|
||||
} else {
|
||||
for (idx in 0 until sources.size()) {
|
||||
val source = sources.getMap(idx)
|
||||
val cacheControl = computeCacheControl(source.getString("cache"))
|
||||
var imageSource =
|
||||
ImageSource(
|
||||
context,
|
||||
source.getString("uri"),
|
||||
source.getDouble("width"),
|
||||
source.getDouble("height"))
|
||||
source.getDouble("height"),
|
||||
cacheControl)
|
||||
if (Uri.EMPTY == imageSource.uri) {
|
||||
warnImageSource(source.getString("uri"))
|
||||
imageSource = getTransparentBitmapImageSource(context)
|
||||
@@ -301,6 +306,15 @@ public class ReactImageView(
|
||||
isDirty = true
|
||||
}
|
||||
|
||||
private fun computeCacheControl(cacheControl: String?): ImageCacheControl {
|
||||
return when (cacheControl) {
|
||||
null,
|
||||
"default" -> ImageCacheControl.DEFAULT
|
||||
"reload" -> ImageCacheControl.RELOAD
|
||||
else -> ImageCacheControl.DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
public fun setDefaultSource(name: String?) {
|
||||
val newDefaultDrawable = instance.getResourceDrawable(context, name)
|
||||
if (defaultImageDrawable != newDefaultDrawable) {
|
||||
@@ -409,7 +423,9 @@ public class ReactImageView(
|
||||
}
|
||||
|
||||
private fun maybeUpdateViewFromRequest(doResize: Boolean) {
|
||||
val uri = this.imageSource?.uri ?: return
|
||||
val imageSource = this.imageSource ?: return
|
||||
val uri = imageSource.uri
|
||||
val cacheControl = imageSource.cacheControl
|
||||
|
||||
val postprocessorList = mutableListOf<Postprocessor>()
|
||||
iterativeBoxBlurPostProcessor?.let { postprocessorList.add(it) }
|
||||
@@ -418,6 +434,11 @@ public class ReactImageView(
|
||||
|
||||
val resizeOptions = if (doResize) resizeOptions else null
|
||||
|
||||
if (cacheControl == ImageCacheControl.RELOAD) {
|
||||
val imagePipeline = Fresco.getImagePipeline()
|
||||
imagePipeline.evictFromCache(uri)
|
||||
}
|
||||
|
||||
val imageRequestBuilder =
|
||||
ImageRequestBuilder.newBuilderWithSource(uri)
|
||||
.setPostprocessor(postprocessor)
|
||||
@@ -430,7 +451,7 @@ public class ReactImageView(
|
||||
}
|
||||
|
||||
val imageRequest: ImageRequest =
|
||||
ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers)
|
||||
ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers, cacheControl)
|
||||
|
||||
globalImageLoadListener?.onLoadAttempt(uri)
|
||||
|
||||
|
||||
+7
-4
@@ -9,6 +9,7 @@ package com.facebook.react.views.imagehelper
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.facebook.react.modules.fresco.ImageCacheControl
|
||||
import java.util.Objects
|
||||
|
||||
/** Class describing an image source (network URI or resource) and size. */
|
||||
@@ -19,7 +20,8 @@ constructor(
|
||||
/** Get the source of this image, as it was passed to the constructor. */
|
||||
public val source: String?,
|
||||
width: Double = 0.0,
|
||||
height: Double = 0.0
|
||||
height: Double = 0.0,
|
||||
public val cacheControl: ImageCacheControl = ImageCacheControl.DEFAULT,
|
||||
) {
|
||||
|
||||
/** Get the URI for this image - can be either a parsed network URI or a resource URI. */
|
||||
@@ -45,10 +47,11 @@ constructor(
|
||||
return java.lang.Double.compare(that.size, size) == 0 &&
|
||||
isResource == that.isResource &&
|
||||
uri == that.uri &&
|
||||
source == that.source
|
||||
source == that.source &&
|
||||
cacheControl == that.cacheControl
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(uri, source, size, isResource)
|
||||
override fun hashCode(): Int = Objects.hash(uri, source, size, isResource, cacheControl)
|
||||
|
||||
private fun computeUri(context: Context): Uri =
|
||||
try {
|
||||
@@ -70,6 +73,6 @@ constructor(
|
||||
|
||||
@JvmStatic
|
||||
public fun getTransparentBitmapImageSource(context: Context): ImageSource =
|
||||
ImageSource(context, TRANSPARENT_BITMAP_URI)
|
||||
ImageSource(context, TRANSPARENT_BITMAP_URI, cacheControl = ImageCacheControl.DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import type {ImageProps} from 'react-native/Libraries/Image/ImageProps';
|
||||
import type {LayoutEvent} from 'react-native/Libraries/Types/CoreEventTypes';
|
||||
|
||||
import RNTesterButton from '../../components/RNTesterButton';
|
||||
import RNTesterText from '../../components/RNTesterText';
|
||||
import ImageCapInsetsExample from './ImageCapInsetsExample';
|
||||
import React from 'react';
|
||||
@@ -626,6 +627,51 @@ class VectorDrawableExample extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
function CacheControlAndroidExample(): React.Node {
|
||||
const [reload, setReload] = React.useState(0);
|
||||
|
||||
const onReload = () => {
|
||||
setReload(prevReload => prevReload + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.horizontal}>
|
||||
<View>
|
||||
<RNTesterText style={styles.resizeModeText}>Default</RNTesterText>
|
||||
<Image
|
||||
source={{
|
||||
uri: fullImage.uri + '?cacheBust=default',
|
||||
cache: 'default',
|
||||
}}
|
||||
style={styles.base}
|
||||
key={reload}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.leftMargin}>
|
||||
<RNTesterText style={styles.resizeModeText}>Reload</RNTesterText>
|
||||
<Image
|
||||
source={{
|
||||
uri: fullImage.uri + '?cacheBust=reload',
|
||||
cache: 'reload',
|
||||
}}
|
||||
style={styles.base}
|
||||
key={reload}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.horizontal}>
|
||||
<View style={styles.cachePolicyAndroidButtonContainer}>
|
||||
<RNTesterButton onPress={onReload}>
|
||||
Re-render image components
|
||||
</RNTesterButton>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const fullImage: ImageSource = {
|
||||
uri: IMAGE2,
|
||||
};
|
||||
@@ -863,6 +909,11 @@ const styles = StyleSheet.create({
|
||||
height: 100,
|
||||
width: '500%',
|
||||
},
|
||||
cachePolicyAndroidButtonContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
},
|
||||
});
|
||||
|
||||
exports.displayName = (undefined: ?string);
|
||||
@@ -1038,6 +1089,16 @@ exports.examples = [
|
||||
},
|
||||
platform: 'ios',
|
||||
},
|
||||
{
|
||||
title: 'Cache Policy',
|
||||
description: ('First image will be loaded and will be cached. ' +
|
||||
'Second image is the same but will be reloaded if re-rendered ' +
|
||||
'as the cache policy is set to reload.': string),
|
||||
render: function (): React.Node {
|
||||
return <CacheControlAndroidExample />;
|
||||
},
|
||||
platform: 'android',
|
||||
},
|
||||
{
|
||||
title: 'Borders',
|
||||
name: 'borders',
|
||||
|
||||
Reference in New Issue
Block a user