diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java index 7d28566e2643..356bc32c0c18 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java @@ -19,7 +19,6 @@ import com.facebook.react.modules.core.PermissionListener; import com.facebook.react.util.AndroidVersion; import org.jetbrains.annotations.NotNull; -import com.facebook.react.uimanager.UIManagerConstantsCache; /** Base Activity for React Native applications. */ public abstract class ReactActivity extends AppCompatActivity @@ -59,7 +58,6 @@ protected ReactActivityDelegate createReactActivityDelegate() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - UIManagerConstantsCache.getInstance().init(this); mDelegate.onCreate(savedInstanceState); if (AndroidVersion.isAtLeastTargetSdk36(this)) { getOnBackPressedDispatcher().addCallback(this, mBackPressedCallback); 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 1835b84e50f6..0bef162c1317 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,6 +220,7 @@ internal class ReactInstance( // initialized. // This happens inside getTurboModuleManagerDelegate getter. if (ReactNativeFeatureFlags.useNativeViewConfigsInBridgelessMode()) { + val customBubblingEvents: MutableMap = HashMap() val customDirectEvents: MutableMap = HashMap() UIConstantsProviderBinding.install( @@ -239,19 +240,30 @@ internal class ReactInstance( getConstantsForViewManager(viewManager, customDirectEvents) }, { - UIManagerConstantsCache.getInstance().getUIManagerConstantsAsWritableMap() - ?: run { - 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 - } - Arguments.makeNativeMap(constants) - } + val constantsCache = UIManagerConstantsCache.getInstance() + val cachedConstants = constantsCache.getUIManagerConstantsAsWritableMap() + if (cachedConstants != null) { + cachedConstants + } else { + val viewManagers: List> = + ArrayList(viewManagerResolver.eagerViewManagerMap.values) + val constants = + createConstants(viewManagers, customBubblingEvents, customDirectEvents) + + val lazyViewManagers = viewManagerResolver.lazyViewManagerNames + if (!lazyViewManagers.isEmpty()) { + constants["ViewManagerNames"] = ArrayList(lazyViewManagers) + constants["LazyViewManagersEnabled"] = true + } + + // Persist only after the constants are complete so a cache hit returns the exact + // same shape as the miss path. + constantsCache.saveConstantsAndBubblingEventsTypes( + constants, + customBubblingEvents, + ) + Arguments.makeNativeMap(constants) + } }, ) } @@ -598,6 +610,7 @@ internal class ReactInstance( private fun createConstants( viewManagers: List>, + customBubblingEvents: MutableMap?, customDirectEvents: MutableMap?, ): MutableMap { ReactMarker.logMarker(ReactMarkerConstants.CREATE_UI_MANAGER_MODULE_CONSTANTS_START) @@ -607,7 +620,7 @@ internal class ReactInstance( try { return UIManagerModuleConstantsHelper.createConstants( viewManagers, - null, + customBubblingEvents, customDirectEvents, ) } finally { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java index 827fa04e203a..22a83ef1dddc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerConstantsCache.java @@ -64,6 +64,9 @@ public class UIManagerConstantsCache { /** Ensures init(...) is only done once. */ private volatile boolean initCalled = false; + /** Allows the host application to keep the cache fully disabled for an experiment control. */ + private volatile boolean cacheEnabled = false; + private String cacheVersionName = null; private UIManagerConstantsCache() { @@ -75,18 +78,33 @@ public static UIManagerConstantsCache getInstance() { } /** - * Must be called (once) from Application or MainActivity before any UIManager - * constants are accessed. This kicks off: + * Must be called once from Application before any UIManager constants are accessed. This kicks + * off: * 1) MMKV.initialize(...) * 2) A background thread that reads two MMKV keys, parses them → Maps, * and countDown()s loadLatch. */ - public synchronized void init(Context appContext) { + public void init(Context appContext) { + init(appContext, true); + } + + /** + * Initializes the cache when enabled. A disabled initialization permanently leaves this + * singleton disabled for the process so later initialization cannot change the experiment + * assignment. + */ + public synchronized void init(Context appContext, boolean enabled) { if (initCalled) { return; } + cacheEnabled = enabled; initCalled = true; + if (!cacheEnabled) { + loadLatch.countDown(); + return; + } + // Its possible that the constants change between versions, so we have to keep track of the version // and reset the cache if it changes. // TODO: This can be improved by us compiling the UIManager constants at compile time @@ -96,6 +114,8 @@ public synchronized void init(Context appContext) { .versionName; } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Failed to get app version name; continuing without caching!", e); + cacheEnabled = false; + loadLatch.countDown(); return; } @@ -183,6 +203,9 @@ public synchronized void init(Context appContext) { * @return parsed Map or null if none stored / parse failed. */ public Map getCachedConstants() { + if (!isCacheEnabled()) { + return null; + } try { loadLatch.await(); } catch (InterruptedException e) { @@ -200,6 +223,9 @@ public Map getCachedConstants() { * @return parsed Map or null if none stored / parse failed. */ public Map getCachedBubblingEventsTypes() { + if (!isCacheEnabled()) { + return null; + } try { loadLatch.await(); } catch (InterruptedException e) { @@ -223,6 +249,9 @@ public Map getCachedBubblingEventsTypes() { * @return WritableNativeMap or null if no full-constants available. */ public WritableNativeMap getUIManagerConstantsAsWritableMap() { + if (!isCacheEnabled()) { + return null; + } try { loadLatch.await(); } catch (InterruptedException e) { @@ -246,6 +275,9 @@ public WritableNativeMap getUIManagerConstantsAsWritableMap() { public void saveConstantsAndBubblingEventsTypes( final Map constants, final Map bubblingEventsTypes) { + if (!isCacheEnabled()) { + return; + } if (constants == null) { return; } @@ -282,6 +314,15 @@ public void saveConstantsAndBubblingEventsTypes( MMKV.defaultMMKV().encode(MMKV_KEY_VERSION, cacheVersionName); } + private boolean isCacheEnabled() { + if (!initCalled) { + throw new IllegalStateException( + "UIManagerConstantsCache not initialized! Call init(...) first." + ); + } + return cacheEnabled; + } + // ──────────────────────────────────────────────────────────────────────────────── // JSON ↔ Map Helpers (identical to before) // ────────────────────────────────────────────────────────────────────────────────