Skip to content

Commit

Permalink
Add custom Fresco decoder for binary XML
Browse files Browse the repository at this point in the history
Summary:
## Summary
Vector drawable decompression is a blocking operation and can sometimes take upwards of 20-30ms per image, especially if that vector drawable uses complex colors. Less complex vector drawables still take several milliseconds to decompress and are served from a cache on later load attempts.

Here's a list of all the load times of vector drawable images in FBVR:
 {F1891592104}

This diff aims to shift decompression to one of Fresco's decode threads, off the main thread, so we don't block while waiting for decompression operations to complete. This relies on adding a new custom decoder that reads the header of XML binary and converts encoded image requests to drawable objects that are yielded from the new `XmlDrawableFactory`.

It's important to note that this change does not stop `ReactImageView` from loading XML-based drawables on the main thread, it merely offers a new mechanism for loading them.

## Changelog
[Android][Added] - Add a new Fresco decoder for XML resource types

Differential Revision: D63476283
  • Loading branch information
Abbondanzo authored and facebook-github-bot committed Sep 27, 2024
1 parent 7c60a65 commit 2185c3b
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 3 deletions.
22 changes: 22 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -3480,6 +3480,28 @@ public final class com/facebook/react/modules/fresco/SystraceRequestListener : c
public fun requiresExtraMap (Ljava/lang/String;)Z
}

public final class com/facebook/react/modules/fresco/XmlFormat {
public static final field INSTANCE Lcom/facebook/react/modules/fresco/XmlFormat;
public final fun getFORMAT ()Lcom/facebook/imageformat/ImageFormat;
}

public final class com/facebook/react/modules/fresco/XmlFormat$XmlDrawableFactory : com/facebook/imagepipeline/drawable/DrawableFactory {
public fun <init> ()V
public fun createDrawable (Lcom/facebook/imagepipeline/image/CloseableImage;)Landroid/graphics/drawable/Drawable;
public fun supportsImageType (Lcom/facebook/imagepipeline/image/CloseableImage;)Z
}

public final class com/facebook/react/modules/fresco/XmlFormat$XmlFormatChecker : com/facebook/imageformat/ImageFormat$FormatChecker {
public fun <init> ()V
public fun determineFormat ([BI)Lcom/facebook/imageformat/ImageFormat;
public fun getHeaderSize ()I
}

public final class com/facebook/react/modules/fresco/XmlFormat$XmlFormatDecoder : com/facebook/imagepipeline/decoder/ImageDecoder {
public fun <init> (Landroid/content/Context;)V
public fun decode (Lcom/facebook/imagepipeline/image/EncodedImage;ILcom/facebook/imagepipeline/image/QualityInfo;Lcom/facebook/imagepipeline/common/ImageDecodeOptions;)Lcom/facebook/imagepipeline/image/CloseableImage;
}

public final class com/facebook/react/modules/i18nmanager/I18nManagerModule : com/facebook/fbreact/specs/NativeI18nManagerSpec {
public fun <init> (Lcom/facebook/react/bridge/ReactApplicationContext;)V
public fun allowRTL (Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
package com.facebook.react.modules.fresco

import com.facebook.common.logging.FLog
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory.newBuilder
import com.facebook.imagepipeline.core.DownsampleMode
import com.facebook.imagepipeline.core.ImagePipeline
import com.facebook.imagepipeline.core.ImagePipelineConfig
import com.facebook.imagepipeline.decoder.ImageDecoderConfig
import com.facebook.imagepipeline.listener.RequestListener
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.common.ReactConstants
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags.loadVectorDrawablesOnImages
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.modules.common.ModuleDataCleaner
import com.facebook.react.modules.network.ForwardingCookieHandler
Expand Down Expand Up @@ -75,10 +78,16 @@ constructor(
val reactContext = reactApplicationContext
reactContext.addLifecycleEventListener(this)
if (!hasBeenInitialized()) {
if (config == null) {
config = getDefaultConfig(reactContext)
val pipelineConfig = config ?: getDefaultConfig(reactContext)
val draweeConfigBuilder = DraweeConfig.newBuilder()
if (loadVectorDrawablesOnImages()) {
draweeConfigBuilder.addCustomDrawableFactory(XmlFormat.XmlDrawableFactory())
}
Fresco.initialize(reactContext.applicationContext, config)
Fresco.initialize(
reactContext.applicationContext,
pipelineConfig,
draweeConfigBuilder.build(),
)
hasBeenInitialized = true
} else if (config != null) {
FLog.w(
Expand Down Expand Up @@ -149,13 +158,21 @@ constructor(
requestListeners.add(SystraceRequestListener())
val client = OkHttpClientProvider.createClient()

// Add support for XML drawable images
val decoderConfigBuilder = ImageDecoderConfig.Builder()
if (loadVectorDrawablesOnImages()) {
decoderConfigBuilder.addDecodingCapability(
XmlFormat.FORMAT, XmlFormat.XmlFormatChecker(), XmlFormat.XmlFormatDecoder(context))
}

// make sure to forward cookies for any requests via the okHttpClient
// so that image requests to endpoints that use cookies still work
val container = OkHttpCompat.getCookieJarContainer(client)
val handler = ForwardingCookieHandler(context)
container.setCookieJar(JavaNetCookieJar(handler))
return newBuilder(context.applicationContext, client)
.setNetworkFetcher(ReactOkHttpNetworkFetcher(client))
.setImageDecoderConfig(decoderConfigBuilder.build())
.setDownsampleMode(DownsampleMode.AUTO)
.setRequestListeners(requestListeners)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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

import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import com.facebook.common.logging.FLog
import com.facebook.imageformat.ImageFormat
import com.facebook.imageformat.ImageFormat.FormatChecker
import com.facebook.imageformat.ImageFormatCheckerUtils
import com.facebook.imagepipeline.common.ImageDecodeOptions
import com.facebook.imagepipeline.decoder.ImageDecoder
import com.facebook.imagepipeline.drawable.DrawableFactory
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.image.DefaultCloseableImage
import com.facebook.imagepipeline.image.EncodedImage
import com.facebook.imagepipeline.image.QualityInfo

public object XmlFormat {
public val FORMAT: ImageFormat = ImageFormat("XML", "xml")
private const val TAG: String = "XmlFormat"
// https://justanapplication.wordpress.com/category/android/android-binary-xml/
private val BINARY_XML_HEADER: ByteArray =
byteArrayOf(
3.toByte(),
0.toByte(),
8.toByte(),
0.toByte(),
)

public class XmlFormatChecker : FormatChecker {
override val headerSize: Int = BINARY_XML_HEADER.size

override fun determineFormat(headerBytes: ByteArray, headerSize: Int): ImageFormat {
return when {
headerSize < BINARY_XML_HEADER.size -> ImageFormat.UNKNOWN
ImageFormatCheckerUtils.startsWithPattern(headerBytes, BINARY_XML_HEADER) -> FORMAT
else -> ImageFormat.UNKNOWN
}
}
}

private class CloseableXmlImage(val name: String, val drawable: Drawable) :
DefaultCloseableImage() {
private var closed = false

override fun getSizeInBytes(): Int {
return getWidth() * getHeight() * 4 // 4 bytes ARGB per pixel
}

override fun close() {
closed = true
}

override fun isClosed(): Boolean {
return closed
}

override fun getWidth(): Int {
return drawable.intrinsicWidth.takeIf { it >= 0 } ?: 0
}

override fun getHeight(): Int {
return drawable.intrinsicHeight.takeIf { it >= 0 } ?: 0
}
}

public class XmlFormatDecoder(private val context: Context) : ImageDecoder {
override fun decode(
encodedImage: EncodedImage,
length: Int,
qualityInfo: QualityInfo,
options: ImageDecodeOptions
): CloseableImage? {
return try {
val xmlResourceName = encodedImage.source ?: error("No source in encoded image")
val xmlResource = Uri.parse(xmlResourceName)
// Android resource names are of the format [res|resources]:/[?package]/[res id]
val xmlResourceId =
xmlResource.pathSegments.lastOrNull()?.toIntOrNull() ?: error("Invalid resource id")
// Use application context to avoid leaking the activity
val drawable = context.applicationContext.resources.getDrawable(xmlResourceId, null)
CloseableXmlImage(xmlResourceName, drawable)
} catch (error: Throwable) {
FLog.e(TAG, "Cannot decode xml ${error}", error)
null
}
}
}

public class XmlDrawableFactory : DrawableFactory {
override fun supportsImageType(image: CloseableImage): Boolean {
return image is CloseableXmlImage
}

override fun createDrawable(image: CloseableImage): Drawable? {
return (image as CloseableXmlImage).drawable
}
}
}

0 comments on commit 2185c3b

Please sign in to comment.