From fc4d9489dcf0ca9de5ec01f1a67cc40b436cc5cc Mon Sep 17 00:00:00 2001 From: TurboSzymon Date: Thu, 12 Mar 2026 21:49:52 +0100 Subject: [PATCH 1/2] Cache UIManager constants in bridgeless new architecture on Android --- .../react/tasks/GenerateEntryPointTask.kt | 2 + .../react/tasks/GenerateEntryPointTaskTest.kt | 2 + .../facebook/react/runtime/ReactInstance.kt | 70 ++- .../uimanager/UIManagerConstantsCache.kt | 512 ++++++++++++++++++ 4 files changed, 573 insertions(+), 13 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.kt diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateEntryPointTask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateEntryPointTask.kt index e62b7be075c4..3cc7e18e509e 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateEntryPointTask.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateEntryPointTask.kt @@ -72,6 +72,7 @@ abstract class GenerateEntryPointTask : DefaultTask() { import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger; + import com.facebook.react.uimanager.UIManagerConstantsCache; import com.facebook.react.views.view.WindowUtilKt; import com.facebook.react.soloader.OpenSourceMergedSoMapping; import com.facebook.soloader.SoLoader; @@ -95,6 +96,7 @@ abstract class GenerateEntryPointTask : DefaultTask() { if ({{packageName}}.BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { DefaultNewArchitectureEntryPoint.load(); + UIManagerConstantsCache.maybePreload(context); } if ({{packageName}}.BuildConfig.IS_EDGE_TO_EDGE_ENABLED) { diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateEntryPointTaskTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateEntryPointTaskTest.kt index dfeca8854210..0d8b65f94d04 100644 --- a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateEntryPointTaskTest.kt +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateEntryPointTaskTest.kt @@ -55,6 +55,7 @@ class GenerateEntryPointTaskTest { import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger; + import com.facebook.react.uimanager.UIManagerConstantsCache; import com.facebook.react.views.view.WindowUtilKt; import com.facebook.react.soloader.OpenSourceMergedSoMapping; import com.facebook.soloader.SoLoader; @@ -78,6 +79,7 @@ class GenerateEntryPointTaskTest { if (com.facebook.react.BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { DefaultNewArchitectureEntryPoint.load(); + UIManagerConstantsCache.maybePreload(context); } if (com.facebook.react.BuildConfig.IS_EDGE_TO_EDGE_ENABLED) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt index a5fdda0edd31..464eaac2dbb1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt @@ -65,6 +65,7 @@ import com.facebook.react.uimanager.DisplayMetricsHolder import com.facebook.react.uimanager.IllegalViewOperationException import com.facebook.react.uimanager.UIConstantsProviderBinding import com.facebook.react.uimanager.UIConstantsProviderBinding.ConstantsForViewManagerProvider +import com.facebook.react.uimanager.UIManagerConstantsCache import com.facebook.react.uimanager.UIManagerModuleConstantsHelper import com.facebook.react.uimanager.ViewManager import com.facebook.react.uimanager.ViewManagerRegistry @@ -219,6 +220,7 @@ internal class ReactInstance( // initialized. // This happens inside getTurboModuleManagerDelegate getter. if (ReactNativeFeatureFlags.useNativeViewConfigsInBridgelessMode()) { + UIManagerConstantsCache.maybePreload(context) val customDirectEvents: MutableMap = HashMap() UIConstantsProviderBinding.install( @@ -232,22 +234,58 @@ internal class ReactInstance( // We want to match this beahavior. { Arguments.makeNativeMap(UIManagerModuleConstantsHelper.defaultExportableEventTypes) }, ConstantsForViewManagerProvider { viewManagerName: String -> + UIManagerConstantsCache + .getCachedConstantsForViewManager( + context, + viewManagerName, + customDirectEvents, + context.sourceURL, + ) + ?.let { cachedConstants -> + return@ConstantsForViewManagerProvider cachedConstants + } + val viewManager = viewManagerResolver.getViewManager(viewManagerName) ?: return@ConstantsForViewManagerProvider null - getConstantsForViewManager(viewManager, customDirectEvents) + val viewManagerConstants = + createConstantsForViewManagerMap(viewManager, customDirectEvents) + UIManagerConstantsCache.saveConstantsForViewManager( + context, + viewManagerName, + viewManagerConstants, + customDirectEvents, + context.sourceURL, + ) + Arguments.makeNativeMap(viewManagerConstants) }, { - val viewManagers: List> = - ArrayList(viewManagerResolver.eagerViewManagerMap.values) - val constants = createConstants(viewManagers, customDirectEvents) - - val lazyViewManagers = viewManagerResolver.lazyViewManagerNames - if (!lazyViewManagers.isEmpty()) { - constants["ViewManagerNames"] = ArrayList(lazyViewManagers) - constants["LazyViewManagersEnabled"] = true + val cachedConstants = + UIManagerConstantsCache.getCachedConstants( + context, + customDirectEvents, + context.sourceURL, + ) + if (cachedConstants != null) { + cachedConstants + } else { + val viewManagers: List> = + ArrayList(viewManagerResolver.eagerViewManagerMap.values) + val constants = createConstants(viewManagers, customDirectEvents) + + val lazyViewManagers = viewManagerResolver.lazyViewManagerNames + if (!lazyViewManagers.isEmpty()) { + constants["ViewManagerNames"] = ArrayList(lazyViewManagers) + constants["LazyViewManagersEnabled"] = true + } + UIManagerConstantsCache.saveConstants( + context, + constants, + customDirectEvents, + context.sourceURL, + ) + Arguments.makeNativeMap(constants) } - Arguments.makeNativeMap(constants) }, ) } @@ -616,6 +654,14 @@ internal class ReactInstance( viewManager: ViewManager<*, *>, customDirectEvents: MutableMap, ): NativeMap { + val viewManagerConstants = createConstantsForViewManagerMap(viewManager, customDirectEvents) + return Arguments.makeNativeMap(viewManagerConstants) + } + + private fun createConstantsForViewManagerMap( + viewManager: ViewManager<*, *>, + customDirectEvents: MutableMap, + ): Map { SystraceMessage.beginSection( Systrace.TRACE_TAG_REACT, "ReactInstance.getConstantsForViewManager", @@ -624,15 +670,13 @@ internal class ReactInstance( .arg("Lazy", true) .flush() try { - val viewManagerConstants: Map = - UIManagerModuleConstantsHelper.createConstantsForViewManager( + return UIManagerModuleConstantsHelper.createConstantsForViewManager( viewManager, null, null, null, customDirectEvents, ) - return Arguments.makeNativeMap(viewManagerConstants) } finally { SystraceMessage.endSection(Systrace.TRACE_TAG_REACT).flush() } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.kt new file mode 100644 index 000000000000..369ce3e1a6d4 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.kt @@ -0,0 +1,512 @@ +/* + * 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.uimanager + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import com.facebook.common.logging.FLog +import com.facebook.react.BuildConfig +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.NativeMap +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import java.io.File +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.Future +import org.json.JSONArray +import org.json.JSONObject + +internal object UIManagerConstantsCache { + private const val TAG = "UIManagerConstantsCache" + private const val CACHE_SCHEMA_VERSION = 3 + private const val CACHE_FILE_NAME = "uimanager_constants_cache_v3.json" + + private val cacheExecutor: ExecutorService = + Executors.newSingleThreadExecutor { runnable -> + Thread(runnable, "UIManagerConstantsCache").apply { isDaemon = true } + } + private val preloadLock = Any() + @Volatile private var preloadFuture: Future? = null + + @JvmStatic + fun maybePreload(context: Context) { + if (!isCachingEnabled()) { + return + } + ensurePreload(context.applicationContext) + } + + fun getCachedConstants( + context: Context, + customDirectEvents: MutableMap, + bundleSourceUrl: String?, + ): NativeMap? { + val prepared = awaitPreload(context.applicationContext) ?: return null + val currentIdentity = + createCacheIdentity(context.applicationContext, bundleSourceUrl) ?: return null + if (prepared.cacheIdentity != currentIdentity) { + return null + } + prepared.customDirectEvents.forEach { (name, value) -> customDirectEvents[name] = value } + return prepared.constantsNativeMap + } + + fun getCachedConstantsForViewManager( + context: Context, + viewManagerName: String, + customDirectEvents: MutableMap, + bundleSourceUrl: String?, + ): NativeMap? { + val prepared = awaitPreload(context.applicationContext) ?: return null + val currentIdentity = + createCacheIdentity(context.applicationContext, bundleSourceUrl) ?: return null + if (prepared.cacheIdentity != currentIdentity) { + return null + } + prepared.customDirectEvents.forEach { (name, value) -> customDirectEvents[name] = value } + return prepared.lazyViewManagerNativeMaps[viewManagerName] + } + + fun saveConstants( + context: Context, + constants: Map, + customDirectEvents: Map, + bundleSourceUrl: String?, + ) { + if (!isCachingEnabled()) { + return + } + val appContext = context.applicationContext + val cacheIdentity = createCacheIdentity(appContext, bundleSourceUrl) ?: return + val constantsCopy = deepCopyMap(constants) + val eventsCopy = deepCopyMap(customDirectEvents) + + maybePreload(appContext) + cacheExecutor.execute { + runCatching { + val cacheDocument = readCacheDocument(appContext) ?: CacheDocument() + cacheDocument.cacheIdentity = cacheIdentity + cacheDocument.constants = constantsCopy + cacheDocument.customDirectEvents.clear() + cacheDocument.customDirectEvents.putAll(eventsCopy) + writeCacheDocument(appContext, cacheDocument) + invalidatePreload() + } + .onFailure { throwable -> + FLog.w(TAG, "Unable to persist UIManager constants cache", throwable) + } + } + } + + fun saveConstantsForViewManager( + context: Context, + viewManagerName: String, + constants: Map, + customDirectEvents: Map, + bundleSourceUrl: String?, + ) { + if (!isCachingEnabled()) { + return + } + val appContext = context.applicationContext + val cacheIdentity = createCacheIdentity(appContext, bundleSourceUrl) ?: return + val constantsCopy = deepCopyMap(constants) + val eventsCopy = deepCopyMap(customDirectEvents) + + maybePreload(appContext) + cacheExecutor.execute { + runCatching { + val cacheDocument = readCacheDocument(appContext) ?: CacheDocument() + cacheDocument.cacheIdentity = cacheIdentity + cacheDocument.lazyViewManagerConstants[viewManagerName] = constantsCopy + cacheDocument.customDirectEvents.putAll(eventsCopy) + writeCacheDocument(appContext, cacheDocument) + invalidatePreload() + } + .onFailure { throwable -> + FLog.w(TAG, "Unable to persist lazy ViewManager constants cache", throwable) + } + } + } + + private fun ensurePreload(context: Context): Future { + synchronized(preloadLock) { + preloadFuture?.let { return it } + val future = cacheExecutor.submit { loadPreparedCache(context) } + preloadFuture = future + return future + } + } + + private fun awaitPreload(context: Context): PreparedCache? = + runCatching { ensurePreload(context).get() } + .onFailure { throwable -> + FLog.w(TAG, "Unable to preload UIManager constants cache", throwable) + } + .getOrNull() + + private fun loadPreparedCache(context: Context): PreparedCache? { + if (!isCachingEnabled()) { + return null + } + val cacheDocument = readCacheDocument(context) ?: return null + val cacheIdentity = cacheDocument.cacheIdentity ?: return null + + val constantsNativeMap = + runCatching { Arguments.makeNativeMap(deepCopyMap(cacheDocument.constants)) } + .onFailure { throwable -> + FLog.w(TAG, "Unable to prepare UIManager constants NativeMap from cache", throwable) + } + .getOrNull() ?: return null + + val lazyViewManagerNativeMaps = LinkedHashMap() + cacheDocument.lazyViewManagerConstants.forEach { (viewManagerName, constantsMap) -> + runCatching { Arguments.makeNativeMap(deepCopyMap(constantsMap)) } + .onSuccess { nativeMap -> lazyViewManagerNativeMaps[viewManagerName] = nativeMap } + .onFailure { throwable -> + FLog.w(TAG, "Unable to prepare cached constants for $viewManagerName", throwable) + } + } + + return PreparedCache( + constantsNativeMap, + cacheDocument.customDirectEvents, + lazyViewManagerNativeMaps, + cacheIdentity, + ) + } + + private fun readCacheDocument(context: Context): CacheDocument? { + val cacheFile = getCacheFile(context) + if (!cacheFile.exists()) { + return null + } + return runCatching { + val root = JSONObject(cacheFile.readText()) + if (root.optInt("schemaVersion", -1) != CACHE_SCHEMA_VERSION) { + return null + } + val cacheIdentity = CacheIdentity.fromJson(root.optJSONObject("cacheIdentity")) ?: return null + val constants = toMap(root.optJSONObject("constants") ?: JSONObject()) + val customDirectEvents = + LinkedHashMap(toMap(root.optJSONObject("customDirectEvents") ?: JSONObject())) + val lazyViewManagersRoot = root.optJSONObject("lazyViewManagerConstants") ?: JSONObject() + val lazyViewManagers = LinkedHashMap>() + lazyViewManagersRoot.keys().forEach { viewManagerName -> + val value = lazyViewManagersRoot.optJSONObject(viewManagerName) + if (value != null) { + lazyViewManagers[viewManagerName] = toMap(value) + } + } + CacheDocument(cacheIdentity, constants, customDirectEvents, lazyViewManagers) + } + .onFailure { throwable -> + FLog.w(TAG, "Unable to read cached UIManager constants", throwable) + } + .getOrNull() + } + + private fun writeCacheDocument(context: Context, cacheDocument: CacheDocument) { + val root = + JSONObject().apply { + put("schemaVersion", CACHE_SCHEMA_VERSION) + put("buildType", BuildConfig.BUILD_TYPE) + put("cacheIdentity", cacheDocument.cacheIdentity?.toJson()) + put("constants", toJsonObject(cacheDocument.constants)) + put("customDirectEvents", toJsonObject(cacheDocument.customDirectEvents)) + val lazyViewManagers = JSONObject() + cacheDocument.lazyViewManagerConstants.forEach { (viewManagerName, constantsMap) -> + lazyViewManagers.put(viewManagerName, toJsonObject(constantsMap)) + } + put("lazyViewManagerConstants", lazyViewManagers) + } + + val cacheFile = getCacheFile(context) + val cacheDirectory = cacheFile.parentFile + if (cacheDirectory != null && !cacheDirectory.exists()) { + cacheDirectory.mkdirs() + } + cacheFile.writeText(root.toString()) + } + + private fun getCacheFile(context: Context): File = + File(context.filesDir, "react-native/$CACHE_FILE_NAME") + + private fun invalidatePreload() { + synchronized(preloadLock) { preloadFuture = null } + } + + private fun isCachingEnabled(): Boolean = + ReactNativeFeatureFlags.useNativeViewConfigsInBridgelessMode() + + private fun createCacheIdentity(context: Context, bundleSourceUrl: String?): CacheIdentity? { + if (bundleSourceUrl.isNullOrBlank()) { + return null + } + + val packageInfo = getPackageInfo(context) ?: return null + val bundleUri = runCatching { Uri.parse(bundleSourceUrl) }.getOrNull() + val bundleName = extractBundleName(bundleUri, bundleSourceUrl) + val bundleFileMetadata = resolveBundleFileMetadata(bundleSourceUrl) + val bundleVersion = + bundleUri?.let { extractBundleVersion(it) } + ?: bundleFileMetadata?.let { metadata -> "file:${metadata.size}:${metadata.lastModified}" } + ?: "${packageInfo.versionName ?: "unknown"}:${packageInfo.longVersionCodeCompat}:${packageInfo.lastUpdateTime}" + + return CacheIdentity( + packageName = context.packageName, + appVersionName = packageInfo.versionName ?: "", + appVersionCode = packageInfo.longVersionCodeCompat, + appLastUpdateTime = packageInfo.lastUpdateTime, + bundleSourceUrl = bundleSourceUrl, + bundleName = bundleName, + bundleVersion = bundleVersion, + bundleFileSize = bundleFileMetadata?.size ?: -1L, + bundleFileLastModified = bundleFileMetadata?.lastModified ?: -1L, + ) + } + + private fun getPackageInfo(context: Context): PackageInfo? = + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.getPackageInfo( + context.packageName, + PackageManager.PackageInfoFlags.of(0), + ) + } else { + @Suppress("DEPRECATION") + context.packageManager.getPackageInfo(context.packageName, 0) + } + } + .onFailure { throwable -> FLog.w(TAG, "Unable to read package info", throwable) } + .getOrNull() + + private fun extractBundleName(bundleUri: Uri?, bundleSourceUrl: String): String = + bundleUri?.pathSegments?.lastOrNull() + ?: bundleUri?.lastPathSegment + ?: File(bundleSourceUrl).name.takeIf { it.isNotBlank() } + ?: bundleSourceUrl + + private fun extractBundleVersion(bundleUri: Uri): String? { + val keys = arrayOf("bundleVersion", "version", "v", "rev", "revision", "hash") + keys.forEach { key -> + bundleUri.getQueryParameter(key)?.takeIf { it.isNotBlank() }?.let { return it } + } + return null + } + + private fun resolveBundleFileMetadata(bundleSourceUrl: String?): BundleFileMetadata? { + val bundleFile = resolveBundleFile(bundleSourceUrl) ?: return null + if (!bundleFile.exists() || !bundleFile.isFile) { + return null + } + return BundleFileMetadata(bundleFile.length(), bundleFile.lastModified()) + } + + private fun resolveBundleFile(bundleSourceUrl: String?): File? { + if (bundleSourceUrl.isNullOrBlank()) { + return null + } + + val bundleUri = runCatching { Uri.parse(bundleSourceUrl) }.getOrNull() + when (bundleUri?.scheme?.lowercase()) { + "file" -> bundleUri.path?.let { path -> return File(path) } + null -> { + if (bundleSourceUrl.startsWith("/")) { + return File(bundleSourceUrl) + } + } + else -> return null + } + + return null + } + + private fun toJsonObject(map: Map): JSONObject { + val json = JSONObject() + map.forEach { (key, value) -> json.put(key, toJsonValue(value)) } + return json + } + + private fun toJsonArray(list: List): JSONArray { + val array = JSONArray() + list.forEach { value -> array.put(toJsonValue(value)) } + return array + } + + private fun toJsonValue(value: Any?): Any? = + when (value) { + null -> JSONObject.NULL + is Map<*, *> -> { + val map = LinkedHashMap() + value.forEach { (k, v) -> if (k is String) map[k] = v } + toJsonObject(map) + } + is List<*> -> toJsonArray(value) + is Boolean, is Int, is Long, is Double, is Float, is String -> value + else -> value.toString() + } + + private fun toMap(jsonObject: JSONObject): Map { + val map = LinkedHashMap() + jsonObject.keys().forEach { key -> + val value = fromJsonValue(jsonObject.opt(key)) + if (value != null) { + map[key] = value + } + } + return map + } + + private fun toList(jsonArray: JSONArray): List { + val list = ArrayList(jsonArray.length()) + for (index in 0 until jsonArray.length()) { + val value = fromJsonValue(jsonArray.opt(index)) + if (value != null) { + list.add(value) + } + } + return list + } + + private fun fromJsonValue(value: Any?): Any? = + when (value) { + null, JSONObject.NULL -> null + is JSONObject -> toMap(value) + is JSONArray -> toList(value) + else -> value + } + + @Suppress("UNCHECKED_CAST") + private fun deepCopyValue(value: Any?): Any? = + when (value) { + is Map<*, *> -> { + val map = LinkedHashMap() + value.forEach { (key, item) -> + if (key is String) { + val copied = deepCopyValue(item) + if (copied != null) { + map[key] = copied + } + } + } + map + } + is List<*> -> value.mapNotNull { item -> deepCopyValue(item) } + is String, is Boolean, is Int, is Long, is Double, is Float -> value + null -> null + else -> value.toString() + } + + private fun deepCopyMap(source: Map): Map { + val map = LinkedHashMap(source.size) + source.forEach { (key, value) -> + val copied = deepCopyValue(value) + if (copied != null) { + map[key] = copied + } + } + return map + } + + private data class PreparedCache( + val constantsNativeMap: NativeMap, + val customDirectEvents: Map, + val lazyViewManagerNativeMaps: Map, + val cacheIdentity: CacheIdentity, + ) + + private data class CacheDocument( + var cacheIdentity: CacheIdentity? = null, + var constants: Map = emptyMap(), + val customDirectEvents: MutableMap = LinkedHashMap(), + val lazyViewManagerConstants: MutableMap> = LinkedHashMap(), + ) + + private data class CacheIdentity( + val packageName: String, + val appVersionName: String, + val appVersionCode: Long, + val appLastUpdateTime: Long, + val bundleSourceUrl: String, + val bundleName: String, + val bundleVersion: String, + val bundleFileSize: Long, + val bundleFileLastModified: Long, + ) { + fun toJson(): JSONObject = + JSONObject().apply { + put("packageName", packageName) + put("appVersionName", appVersionName) + put("appVersionCode", appVersionCode) + put("appLastUpdateTime", appLastUpdateTime) + put("bundleSourceUrl", bundleSourceUrl) + put("bundleName", bundleName) + put("bundleVersion", bundleVersion) + put("bundleFileSize", bundleFileSize) + put("bundleFileLastModified", bundleFileLastModified) + } + + companion object { + fun fromJson(jsonObject: JSONObject?): CacheIdentity? { + if (jsonObject == null) { + return null + } + + val packageName = jsonObject.optString("packageName", "") + val appVersionName = jsonObject.optString("appVersionName", "") + val appVersionCode = jsonObject.optLong("appVersionCode", -1) + val appLastUpdateTime = jsonObject.optLong("appLastUpdateTime", -1) + val bundleSourceUrl = jsonObject.optString("bundleSourceUrl", "") + val bundleName = jsonObject.optString("bundleName", "") + val bundleVersion = jsonObject.optString("bundleVersion", "") + val bundleFileSize = jsonObject.optLong("bundleFileSize", -1) + val bundleFileLastModified = jsonObject.optLong("bundleFileLastModified", -1) + + if ( + packageName.isEmpty() || + appVersionCode < 0 || + appLastUpdateTime < 0 || + bundleSourceUrl.isEmpty() || + bundleName.isEmpty() || + bundleVersion.isEmpty() || + bundleFileSize < -1 || + bundleFileLastModified < -1 + ) { + return null + } + + return CacheIdentity( + packageName = packageName, + appVersionName = appVersionName, + appVersionCode = appVersionCode, + appLastUpdateTime = appLastUpdateTime, + bundleSourceUrl = bundleSourceUrl, + bundleName = bundleName, + bundleVersion = bundleVersion, + bundleFileSize = bundleFileSize, + bundleFileLastModified = bundleFileLastModified, + ) + } + } + } + + private data class BundleFileMetadata(val size: Long, val lastModified: Long) + + @get:Suppress("DEPRECATION") + private val PackageInfo.longVersionCodeCompat: Long + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + longVersionCode + } else { + versionCode.toLong() + } +} From 74a915dddf8085b1bf48f04db4bd11238d7fddc6 Mon Sep 17 00:00:00 2001 From: TurboSzymon Date: Mon, 16 Mar 2026 14:28:42 +0100 Subject: [PATCH 2/2] hide behind a feature flag --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 ++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 ++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../facebook/react/runtime/ReactInstance.kt | 81 ++++++++++--------- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 38 ++++++--- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 ++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 11 +++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- .../rn-tester/android/app/build.gradle.kts | 3 + .../react/uiapp/RNTesterApplication.kt | 10 +++ 23 files changed, 203 insertions(+), 66 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 7be4cb067bba..d2bc92382e0c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5144fb0350b71394206d614c68ef87f0>> + * @generated SignedSource<<5765eb9864a1280e5c8706a8cfb22e5b>> */ /** @@ -516,6 +516,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun useNativeViewConfigsInBridgelessMode(): Boolean = accessor.useNativeViewConfigsInBridgelessMode() + /** + * When enabled, bridgeless mode skips the native persisted UIManager constants cache and always computes constants on demand. + */ + @JvmStatic + public fun disableNativeUIManagerConstantsCacheInBridgelessMode(): Boolean = accessor.disableNativeUIManagerConstantsCacheInBridgelessMode() + /** * When enabled, ReactScrollView will extend NestedScrollView instead of ScrollView on Android for improved nested scrolling support. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index a77a2ff90fe9..d8d4a52e261f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<06438b87ea376f37c31a272e8dc04597>> */ /** @@ -101,6 +101,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null private var useFabricInteropCache: Boolean? = null private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null + private var disableNativeUIManagerConstantsCacheInBridgelessModeCache: Boolean? = null private var useNestedScrollViewAndroidCache: Boolean? = null private var useSharedAnimatedBackendCache: Boolean? = null private var useTraitHiddenOnAndroidCache: Boolean? = null @@ -840,6 +841,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun disableNativeUIManagerConstantsCacheInBridgelessMode(): Boolean { + var cached = disableNativeUIManagerConstantsCacheInBridgelessModeCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.disableNativeUIManagerConstantsCacheInBridgelessMode() + disableNativeUIManagerConstantsCacheInBridgelessModeCache = cached + } + return cached + } + override fun useNestedScrollViewAndroid(): Boolean { var cached = useNestedScrollViewAndroidCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index fa8758c0901b..15468dce4bb2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<1e88e791ede99c1a251a37405bafdbef>> */ /** @@ -190,6 +190,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun useNativeViewConfigsInBridgelessMode(): Boolean + @DoNotStrip @JvmStatic public external fun disableNativeUIManagerConstantsCacheInBridgelessMode(): Boolean + @DoNotStrip @JvmStatic public external fun useNestedScrollViewAndroid(): Boolean @DoNotStrip @JvmStatic public external fun useSharedAnimatedBackend(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index e467f8cd7327..ce6c2dffe02b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -185,6 +185,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun useNativeViewConfigsInBridgelessMode(): Boolean = false + override fun disableNativeUIManagerConstantsCacheInBridgelessMode(): Boolean = true + override fun useNestedScrollViewAndroid(): Boolean = false override fun useSharedAnimatedBackend(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 99d211c64a30..586d2e888faa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7b87f5541ecf881d8ce51c5edd5b99b0>> + * @generated SignedSource<> */ /** @@ -105,6 +105,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var useAlwaysAvailableJSErrorHandlingCache: Boolean? = null private var useFabricInteropCache: Boolean? = null private var useNativeViewConfigsInBridgelessModeCache: Boolean? = null + private var disableNativeUIManagerConstantsCacheInBridgelessModeCache: Boolean? = null private var useNestedScrollViewAndroidCache: Boolean? = null private var useSharedAnimatedBackendCache: Boolean? = null private var useTraitHiddenOnAndroidCache: Boolean? = null @@ -925,6 +926,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun disableNativeUIManagerConstantsCacheInBridgelessMode(): Boolean { + var cached = disableNativeUIManagerConstantsCacheInBridgelessModeCache + if (cached == null) { + cached = currentProvider.disableNativeUIManagerConstantsCacheInBridgelessMode() + accessedFeatureFlags.add("disableNativeUIManagerConstantsCacheInBridgelessMode") + disableNativeUIManagerConstantsCacheInBridgelessModeCache = cached + } + return cached + } + override fun useNestedScrollViewAndroid(): Boolean { var cached = useNestedScrollViewAndroidCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index de1d05f86ef3..efe044ab63a3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -185,6 +185,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun useNativeViewConfigsInBridgelessMode(): Boolean + @DoNotStrip public fun disableNativeUIManagerConstantsCacheInBridgelessMode(): Boolean + @DoNotStrip public fun useNestedScrollViewAndroid(): Boolean @DoNotStrip public fun useSharedAnimatedBackend(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt index 464eaac2dbb1..53f47cd73305 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactInstance.kt @@ -220,7 +220,11 @@ internal class ReactInstance( // initialized. // This happens inside getTurboModuleManagerDelegate getter. if (ReactNativeFeatureFlags.useNativeViewConfigsInBridgelessMode()) { - UIManagerConstantsCache.maybePreload(context) + val shouldUseNativeUIManagerConstantsCache = + !ReactNativeFeatureFlags.disableNativeUIManagerConstantsCacheInBridgelessMode() + if (shouldUseNativeUIManagerConstantsCache) { + UIManagerConstantsCache.maybePreload(context) + } val customDirectEvents: MutableMap = HashMap() UIConstantsProviderBinding.install( @@ -234,58 +238,61 @@ internal class ReactInstance( // We want to match this beahavior. { Arguments.makeNativeMap(UIManagerModuleConstantsHelper.defaultExportableEventTypes) }, ConstantsForViewManagerProvider { viewManagerName: String -> - UIManagerConstantsCache - .getCachedConstantsForViewManager( - context, - viewManagerName, - customDirectEvents, - context.sourceURL, - ) - ?.let { cachedConstants -> - return@ConstantsForViewManagerProvider cachedConstants - } + if (shouldUseNativeUIManagerConstantsCache) { + UIManagerConstantsCache + .getCachedConstantsForViewManager( + context, + viewManagerName, + customDirectEvents, + context.sourceURL, + ) + ?.let { cachedConstants -> return@ConstantsForViewManagerProvider cachedConstants } + } val viewManager = viewManagerResolver.getViewManager(viewManagerName) ?: return@ConstantsForViewManagerProvider null val viewManagerConstants = createConstantsForViewManagerMap(viewManager, customDirectEvents) - UIManagerConstantsCache.saveConstantsForViewManager( - context, - viewManagerName, - viewManagerConstants, - customDirectEvents, - context.sourceURL, - ) + if (shouldUseNativeUIManagerConstantsCache) { + UIManagerConstantsCache.saveConstantsForViewManager( + context, + viewManagerName, + viewManagerConstants, + customDirectEvents, + context.sourceURL, + ) + } Arguments.makeNativeMap(viewManagerConstants) }, { - val cachedConstants = - UIManagerConstantsCache.getCachedConstants( - context, - customDirectEvents, - context.sourceURL, - ) - if (cachedConstants != null) { - cachedConstants - } else { - val viewManagers: List> = - ArrayList(viewManagerResolver.eagerViewManagerMap.values) - val constants = createConstants(viewManagers, customDirectEvents) - - val lazyViewManagers = viewManagerResolver.lazyViewManagerNames - if (!lazyViewManagers.isEmpty()) { - constants["ViewManagerNames"] = ArrayList(lazyViewManagers) - constants["LazyViewManagersEnabled"] = true - } + if (shouldUseNativeUIManagerConstantsCache) { + UIManagerConstantsCache + .getCachedConstants( + context, + customDirectEvents, + context.sourceURL, + ) + ?.let { cachedConstants -> return@install cachedConstants } + } + val viewManagers: List> = + ArrayList(viewManagerResolver.eagerViewManagerMap.values) + val constants = createConstants(viewManagers, customDirectEvents) + + val lazyViewManagers = viewManagerResolver.lazyViewManagerNames + if (!lazyViewManagers.isEmpty()) { + constants["ViewManagerNames"] = ArrayList(lazyViewManagers) + constants["LazyViewManagersEnabled"] = true + } + if (shouldUseNativeUIManagerConstantsCache) { UIManagerConstantsCache.saveConstants( context, constants, customDirectEvents, context.sourceURL, ) - Arguments.makeNativeMap(constants) } + Arguments.makeNativeMap(constants) }, ) } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index fe397f8b1e41..f6f43449db46 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<82fae58a4b2ebca771351029b6e21265>> */ /** @@ -525,6 +525,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool disableNativeUIManagerConstantsCacheInBridgelessMode() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("disableNativeUIManagerConstantsCacheInBridgelessMode"); + return method(javaProvider_); + } + bool useNestedScrollViewAndroid() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("useNestedScrollViewAndroid"); @@ -988,6 +994,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useNativeViewConfigsInBridgelessMode( return ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode(); } +bool JReactNativeFeatureFlagsCxxInterop::disableNativeUIManagerConstantsCacheInBridgelessMode( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::disableNativeUIManagerConstantsCacheInBridgelessMode(); +} + bool JReactNativeFeatureFlagsCxxInterop::useNestedScrollViewAndroid( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::useNestedScrollViewAndroid(); @@ -1307,6 +1318,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "useNativeViewConfigsInBridgelessMode", JReactNativeFeatureFlagsCxxInterop::useNativeViewConfigsInBridgelessMode), + makeNativeMethod( + "disableNativeUIManagerConstantsCacheInBridgelessMode", + JReactNativeFeatureFlagsCxxInterop::disableNativeUIManagerConstantsCacheInBridgelessMode), makeNativeMethod( "useNestedScrollViewAndroid", JReactNativeFeatureFlagsCxxInterop::useNestedScrollViewAndroid), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 08276eab5edd..71baafcf7cb0 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5433b4a2f4a0574591a38017422edac8>> + * @generated SignedSource<<80fa9f828f4c1bfd9e2da3e7ab9fd883>> */ /** @@ -273,6 +273,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool useNativeViewConfigsInBridgelessMode( facebook::jni::alias_ref); + static bool disableNativeUIManagerConstantsCacheInBridgelessMode( + facebook::jni::alias_ref); + static bool useNestedScrollViewAndroid( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 4f058038abbd..887e4d7b5440 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<0316d70bc847f2aa1a720671d3245926>> */ /** @@ -350,6 +350,10 @@ bool ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode() { return getAccessor().useNativeViewConfigsInBridgelessMode(); } +bool ReactNativeFeatureFlags::disableNativeUIManagerConstantsCacheInBridgelessMode() { + return getAccessor().disableNativeUIManagerConstantsCacheInBridgelessMode(); +} + bool ReactNativeFeatureFlags::useNestedScrollViewAndroid() { return getAccessor().useNestedScrollViewAndroid(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 7185d625c251..7e064870d6fc 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4811a81c7839f2be5c8a127e6c8e310b>> + * @generated SignedSource<> */ /** @@ -444,6 +444,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool useNativeViewConfigsInBridgelessMode(); + /** + * When enabled, bridgeless mode skips the native persisted UIManager constants cache and always computes constants on demand. + */ + RN_EXPORT static bool disableNativeUIManagerConstantsCacheInBridgelessMode(); + /** * When enabled, ReactScrollView will extend NestedScrollView instead of ScrollView on Android for improved nested scrolling support. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 936e3c590d50..25bdbe216199 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<475d7af9dc5ad017007ad8d68ee8d3f9>> */ /** @@ -1487,6 +1487,24 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::disableNativeUIManagerConstantsCacheInBridgelessMode() { + auto flagValue = disableNativeUIManagerConstantsCacheInBridgelessMode_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(81, "disableNativeUIManagerConstantsCacheInBridgelessMode"); + + flagValue = currentProvider_->disableNativeUIManagerConstantsCacheInBridgelessMode(); + disableNativeUIManagerConstantsCacheInBridgelessMode_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { auto flagValue = useNestedScrollViewAndroid_.load(); @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "useNestedScrollViewAndroid"); + markFlagAsAccessed(82, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "useSharedAnimatedBackend"); + markFlagAsAccessed(83, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(84, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "useTurboModuleInterop"); + markFlagAsAccessed(85, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useTurboModules"); + markFlagAsAccessed(86, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useUnorderedMapInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useUnorderedMapInDifferentiator"); + markFlagAsAccessed(87, "useUnorderedMapInDifferentiator"); flagValue = currentProvider_->useUnorderedMapInDifferentiator(); useUnorderedMapInDifferentiator_ = flagValue; @@ -1604,7 +1622,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "viewCullingOutsetRatio"); + markFlagAsAccessed(88, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "viewTransitionEnabled"); + markFlagAsAccessed(89, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1640,7 +1658,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "virtualViewPrerenderRatio"); + markFlagAsAccessed(90, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index e76f1322f58d..87977f6259d4 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<06dba77b9b76d06d5c338ed8a97e33f5>> + * @generated SignedSource<<03a3adfe3b4e0c0952096cf6192f9bf1>> */ /** @@ -113,6 +113,7 @@ class ReactNativeFeatureFlagsAccessor { bool useAlwaysAvailableJSErrorHandling(); bool useFabricInterop(); bool useNativeViewConfigsInBridgelessMode(); + bool disableNativeUIManagerConstantsCacheInBridgelessMode(); bool useNestedScrollViewAndroid(); bool useSharedAnimatedBackend(); bool useTraitHiddenOnAndroid(); @@ -133,7 +134,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 90> accessedFeatureFlags_; + std::array, 91> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -216,6 +217,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> useAlwaysAvailableJSErrorHandling_; std::atomic> useFabricInterop_; std::atomic> useNativeViewConfigsInBridgelessMode_; + std::atomic> disableNativeUIManagerConstantsCacheInBridgelessMode_; std::atomic> useNestedScrollViewAndroid_; std::atomic> useSharedAnimatedBackend_; std::atomic> useTraitHiddenOnAndroid_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 1a6289888d15..119cca2b53b8 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0934d867533630904fc69e30e7a929b3>> + * @generated SignedSource<<404525ada3a4a3569a7f866ec8ed0a1d>> */ /** @@ -351,6 +351,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool disableNativeUIManagerConstantsCacheInBridgelessMode() override { + return true; + } + bool useNestedScrollViewAndroid() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 5c939775920a..8df286d9e76c 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<25d1f9cb509dbd8274e3a00237d2ea62>> + * @generated SignedSource<<33da11ecc8965dc808d5002c6eaa067f>> */ /** @@ -774,6 +774,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::useNativeViewConfigsInBridgelessMode(); } + bool disableNativeUIManagerConstantsCacheInBridgelessMode() override { + auto value = values_["disableNativeUIManagerConstantsCacheInBridgelessMode"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::disableNativeUIManagerConstantsCacheInBridgelessMode(); + } + bool useNestedScrollViewAndroid() override { auto value = values_["useNestedScrollViewAndroid"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index cc3fbc19b88d..d9f180f5e78e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<943038b007fb5b398fa7ff6e5975c243>> */ /** @@ -106,6 +106,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool useAlwaysAvailableJSErrorHandling() = 0; virtual bool useFabricInterop() = 0; virtual bool useNativeViewConfigsInBridgelessMode() = 0; + virtual bool disableNativeUIManagerConstantsCacheInBridgelessMode() = 0; virtual bool useNestedScrollViewAndroid() = 0; virtual bool useSharedAnimatedBackend() = 0; virtual bool useTraitHiddenOnAndroid() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 1fbd4198e5d2..4fff35392954 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0b3534a570416860aa1ffc7e1d808090>> + * @generated SignedSource<<0caed2f334059594985ea395589ab512>> */ /** @@ -449,6 +449,11 @@ bool NativeReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode( return ReactNativeFeatureFlags::useNativeViewConfigsInBridgelessMode(); } +bool NativeReactNativeFeatureFlags::disableNativeUIManagerConstantsCacheInBridgelessMode( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::disableNativeUIManagerConstantsCacheInBridgelessMode(); +} + bool NativeReactNativeFeatureFlags::useNestedScrollViewAndroid( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::useNestedScrollViewAndroid(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 081db38ca2fd..c4f53121da75 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0aeea7c4fa2a8aa4180c83bbd0746250>> + * @generated SignedSource<<766beab931a683fc96879f9aee4f9316>> */ /** @@ -198,6 +198,8 @@ class NativeReactNativeFeatureFlags bool useNativeViewConfigsInBridgelessMode(jsi::Runtime& runtime); + bool disableNativeUIManagerConstantsCacheInBridgelessMode(jsi::Runtime& runtime); + bool useNestedScrollViewAndroid(jsi::Runtime& runtime); bool useSharedAnimatedBackend(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 78dbf32ad3f4..8cdbb10e548d 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -912,6 +912,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'canary', }, + disableNativeUIManagerConstantsCacheInBridgelessMode: { + defaultValue: true, + metadata: { + dateAdded: '2026-03-13', + description: + 'When enabled, bridgeless mode skips the native persisted UIManager constants cache and always computes constants on demand.', + expectedReleaseValue: false, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, useNestedScrollViewAndroid: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 67e7bcbbe95e..d5143e4d1a84 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0fffffdeaf8ab7210131ac789f8b208b>> + * @generated SignedSource<<5b9a77b03afabd12aa8f426756ffaedc>> * @flow strict * @noformat */ @@ -128,6 +128,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ useAlwaysAvailableJSErrorHandling: Getter, useFabricInterop: Getter, useNativeViewConfigsInBridgelessMode: Getter, + disableNativeUIManagerConstantsCacheInBridgelessMode: Getter, useNestedScrollViewAndroid: Getter, useSharedAnimatedBackend: Getter, useTraitHiddenOnAndroid: Getter, @@ -527,6 +528,10 @@ export const useFabricInterop: Getter = createNativeFlagGetter('useFabr * When enabled, the native view configs are used in bridgeless mode. */ export const useNativeViewConfigsInBridgelessMode: Getter = createNativeFlagGetter('useNativeViewConfigsInBridgelessMode', false); +/** + * When enabled, bridgeless mode skips the native persisted UIManager constants cache and always computes constants on demand. + */ +export const disableNativeUIManagerConstantsCacheInBridgelessMode: Getter = createNativeFlagGetter('disableNativeUIManagerConstantsCacheInBridgelessMode', true); /** * When enabled, ReactScrollView will extend NestedScrollView instead of ScrollView on Android for improved nested scrolling support. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index ea68b2cf9eb3..f0cda49203ae 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<85ddaee522fc3be8546aef21cf236e9a>> + * @generated SignedSource<<9fd6d507579d6800e606e1624b6165ca>> * @flow strict * @noformat */ @@ -106,6 +106,7 @@ export interface Spec extends TurboModule { +useAlwaysAvailableJSErrorHandling?: () => boolean; +useFabricInterop?: () => boolean; +useNativeViewConfigsInBridgelessMode?: () => boolean; + +disableNativeUIManagerConstantsCacheInBridgelessMode?: () => boolean; +useNestedScrollViewAndroid?: () => boolean; +useSharedAnimatedBackend?: () => boolean; +useTraitHiddenOnAndroid?: () => boolean; diff --git a/packages/rn-tester/android/app/build.gradle.kts b/packages/rn-tester/android/app/build.gradle.kts index fc1770cbd068..185100fe9f34 100644 --- a/packages/rn-tester/android/app/build.gradle.kts +++ b/packages/rn-tester/android/app/build.gradle.kts @@ -15,6 +15,8 @@ plugins { val reactNativeDirPath = "$rootDir/packages/react-native" val isNewArchEnabled = project.property("newArchEnabled") == "true" +val uiManagerCacheBackend = + (project.findProperty("uiManagerCacheBackend")?.toString() ?: "none").lowercase() /** * This is the configuration block to customize your React Native Android app. By default you don't @@ -116,6 +118,7 @@ android { buildConfigField("String", "JS_MAIN_MODULE_NAME", "\"js/RNTesterApp.android\"") buildConfigField("String", "BUNDLE_ASSET_NAME", "\"RNTesterApp.android.bundle\"") buildConfigField("Boolean", "IS_INTERNAL_BUILD", "false") + buildConfigField("String", "UI_MANAGER_CACHE_BACKEND", "\"$uiManagerCacheBackend\"") } externalNativeBuild { cmake { version = cmakeVersion } } splits { diff --git a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt index 2a162c4c2493..c9fb4c3e0ac8 100644 --- a/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt +++ b/packages/rn-tester/android/app/src/main/java/com/facebook/react/uiapp/RNTesterApplication.kt @@ -23,6 +23,8 @@ import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.common.assets.ReactFontManager import com.facebook.react.defaults.DefaultReactHost +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uiapp.component.MyLegacyViewManager @@ -122,5 +124,13 @@ internal class RNTesterApplication : Application(), ReactApplication { ReactFontManager.getInstance().addCustomFont(this, "FiraCode", R.font.firacode) super.onCreate() loadReactNative(this) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + ReactNativeFeatureFlags.dangerouslyForceOverride( + object : ReactNativeFeatureFlagsOverrides_RNOSS_Canary_Android() { + override fun disableNativeUIManagerConstantsCacheInBridgelessMode(): Boolean = + BuildConfig.UI_MANAGER_CACHE_BACKEND != "native" + } + ) + } } }