From c6d6fd048570c6be9114cda2845f89117991182b Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Wed, 28 Oct 2026 22:50:16 +0100 Subject: [PATCH 1/2] Create one JNI global reference per prepared text layout on Android Every text layout created several JNI global references: TextLayoutManager read FabricUIManager from the context container, which copies its global reference, in prepareLayout, measurePreparedLayout and the other measure paths, and PreparedTextLayout was itself a global reference, so returning a cached layout and storing it in a node's state each created another one. PreparedTextLayout now holds its reference in a shared_ptr, so copies share it, and TextLayoutManager looks FabricUIManager up once. Co-Authored-By: Claude Opus 5.5 --- .../textlayoutmanager/TextLayoutManager.cpp | 30 ++++++++----------- .../textlayoutmanager/TextLayoutManager.h | 29 +++++++++++++++++- .../ReactAndroidFrameworksCxx.api | 8 ++++- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index 23640f6dc641..a18ffc986f0b 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -40,7 +40,7 @@ int countAttachments(const AttributedString& attributedString) { } Size measureText( - const std::shared_ptr& contextContainer, + jobject fabricUIManager, MapBuffer attributedString, MapBuffer paragraphAttributes, float minWidth, @@ -48,9 +48,6 @@ Size measureText( float minHeight, float maxHeight, jfloatArray attachmentPositions) { - const jni::global_ref& fabricUIManager = - contextContainer->at>("FabricUIManager"); - static auto measure = jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") ->getMethod& contextContainer, + jobject fabricUIManager, const AttributedString& attributedString, const ParagraphAttributes& paragraphAttributes, const TextLayoutContext& layoutContext, @@ -101,7 +98,7 @@ TextMeasurement doMeasure( auto paragraphAttributesMap = toMapBuffer(paragraphAttributes); auto size = measureText( - contextContainer, + fabricUIManager, std::move(attributedStringMap), std::move(paragraphAttributesMap), minimumSize.width, @@ -153,6 +150,9 @@ TextMeasurement doMeasure( TextLayoutManager::TextLayoutManager( const std::shared_ptr& contextContainer) : contextContainer_(std::move(contextContainer)), + fabricUIManager_( + contextContainer_->find>("FabricUIManager") + .value_or(jni::global_ref{})), textMeasureCache_(kSimpleThreadSafeCacheSizeCap), lineMeasureCache_(kSimpleThreadSafeCacheSizeCap), preparedTextCache_( @@ -173,7 +173,7 @@ TextMeasurement TextLayoutManager::measure( } auto measurement = doMeasure( - contextContainer_, + fabricUIManager_.get(), attributedString, paragraphAttributes, layoutContext, @@ -217,7 +217,7 @@ TextMeasurement TextLayoutManager::measureCachedSpannableById( localDataBuilder.putInt(AS_KEY_CACHE_ID, static_cast(cacheId)); auto size = measureText( - contextContainer_, + fabricUIManager_.get(), localDataBuilder.build(), toMapBuffer(paragraphAttributes), minimumSize.width, @@ -245,8 +245,7 @@ LinesMeasurements TextLayoutManager::measureLines( const auto& attributedString = attributedStringBox.getValue(); auto doMeasureLines = [&]() { - const jni::global_ref& fabricUIManager = - contextContainer_->at>("FabricUIManager"); + jobject fabricUIManager = fabricUIManager_.get(); static auto measureLines = jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") ->getMethodat>("FabricUIManager"); + jobject fabricUIManager = fabricUIManager_.get(); auto attributedStringMB = JReadableMapBuffer::createWithContents( toMapBuffer(attributedString)); auto paragraphAttributesMB = JReadableMapBuffer::createWithContents( @@ -364,13 +362,12 @@ TextLayoutManager::PreparedTextLayout TextLayoutManager::prepareLayout( javaReactTags->setRegion( 0, static_cast(reactTags.size()), reactTags.data()); - const auto& fabricUIManager = - contextContainer_->at>("FabricUIManager"); + jobject fabricUIManager = fabricUIManager_.get(); return PreparedTextLayout{ jni::make_global(reusePreparedLayoutWithNewReactTags( fabricUIManager, preparedText->get(), javaReactTags.get()))}; } else { - return PreparedTextLayout{*preparedText}; + return *preparedText; } } @@ -378,8 +375,7 @@ TextMeasurement TextLayoutManager::measurePreparedLayout( const PreparedTextLayout& preparedLayout, const TextLayoutContext& /*layoutContext*/, const LayoutConstraints& layoutConstraints) const { - const auto& fabricUIManager = - contextContainer_->at>("FabricUIManager"); + jobject fabricUIManager = fabricUIManager_.get(); static auto measurePreparedLayout = jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h index fd7b1e2f9fd7..1345772ab5c0 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -32,7 +32,31 @@ class TextLayoutManager; */ class TextLayoutManager { public: - using PreparedTextLayout = SafeReleaseJniRef>; + /* + * A prepared layout's Java object. Copies share one JNI global reference, so + * returning a cached layout or storing it in state doesn't create another. + */ + class PreparedTextLayout { + public: + PreparedTextLayout() = default; + explicit PreparedTextLayout(jni::global_ref ref) + : ref_(std::make_shared>>(std::move(ref))) + { + } + + JPreparedLayout::javaobject get() const + { + return ref_ ? ref_->get() : nullptr; + } + + explicit operator bool() const noexcept + { + return ref_ && *ref_; + } + + private: + std::shared_ptr>> ref_; + }; TextLayoutManager(const std::shared_ptr &contextContainer); @@ -96,6 +120,9 @@ class TextLayoutManager { private: std::shared_ptr contextContainer_; + // FabricUIManager, read from the context container once: reading it copies + // its JNI global reference. + const SafeReleaseJniRef> fabricUIManager_; TextMeasureCache textMeasureCache_; LineMeasureCache lineMeasureCache_; SimpleThreadSafeCache preparedTextCache_; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index 5e5dad5f5ce5..16c6a2d49520 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -4012,7 +4012,13 @@ class facebook::react::TextLayoutManager { public facebook::react::TextMeasurement measure(const facebook::react::AttributedStringBox& attributedStringBox, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; public facebook::react::TextMeasurement measureCachedSpannableById(int64_t cacheId, const facebook::react::ParagraphAttributes& paragraphAttributes, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; public facebook::react::TextMeasurement measurePreparedLayout(const facebook::react::TextLayoutManager::PreparedTextLayout& layout, const facebook::react::TextLayoutContext& layoutContext, const facebook::react::LayoutConstraints& layoutConstraints) const; - public using PreparedTextLayout = facebook::react::SafeReleaseJniRef>; +} + +class facebook::react::TextLayoutManager::PreparedTextLayout { + public JPreparedLayout::javaobject get() const; + public PreparedTextLayout() = default; + public PreparedTextLayout(jni::global_ref ref); + public operator bool() const noexcept; } class facebook::react::TextMeasureCacheKey { From 6860e2454637c2608d71905fa05deca3ae736fcc Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Thu, 29 Oct 2026 12:29:00 +0100 Subject: [PATCH 2/2] Keep the FabricUIManager lookup out of this change Reading FabricUIManager from the context container once will be done separately; this change now only shares one JNI global reference between copies of a prepared text layout. Co-Authored-By: Claude Opus 5.5 --- .../textlayoutmanager/TextLayoutManager.cpp | 30 +++++++++++-------- .../textlayoutmanager/TextLayoutManager.h | 3 -- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index a18ffc986f0b..23640f6dc641 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -40,7 +40,7 @@ int countAttachments(const AttributedString& attributedString) { } Size measureText( - jobject fabricUIManager, + const std::shared_ptr& contextContainer, MapBuffer attributedString, MapBuffer paragraphAttributes, float minWidth, @@ -48,6 +48,9 @@ Size measureText( float minHeight, float maxHeight, jfloatArray attachmentPositions) { + const jni::global_ref& fabricUIManager = + contextContainer->at>("FabricUIManager"); + static auto measure = jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") ->getMethod& contextContainer, const AttributedString& attributedString, const ParagraphAttributes& paragraphAttributes, const TextLayoutContext& layoutContext, @@ -98,7 +101,7 @@ TextMeasurement doMeasure( auto paragraphAttributesMap = toMapBuffer(paragraphAttributes); auto size = measureText( - fabricUIManager, + contextContainer, std::move(attributedStringMap), std::move(paragraphAttributesMap), minimumSize.width, @@ -150,9 +153,6 @@ TextMeasurement doMeasure( TextLayoutManager::TextLayoutManager( const std::shared_ptr& contextContainer) : contextContainer_(std::move(contextContainer)), - fabricUIManager_( - contextContainer_->find>("FabricUIManager") - .value_or(jni::global_ref{})), textMeasureCache_(kSimpleThreadSafeCacheSizeCap), lineMeasureCache_(kSimpleThreadSafeCacheSizeCap), preparedTextCache_( @@ -173,7 +173,7 @@ TextMeasurement TextLayoutManager::measure( } auto measurement = doMeasure( - fabricUIManager_.get(), + contextContainer_, attributedString, paragraphAttributes, layoutContext, @@ -217,7 +217,7 @@ TextMeasurement TextLayoutManager::measureCachedSpannableById( localDataBuilder.putInt(AS_KEY_CACHE_ID, static_cast(cacheId)); auto size = measureText( - fabricUIManager_.get(), + contextContainer_, localDataBuilder.build(), toMapBuffer(paragraphAttributes), minimumSize.width, @@ -245,7 +245,8 @@ LinesMeasurements TextLayoutManager::measureLines( const auto& attributedString = attributedStringBox.getValue(); auto doMeasureLines = [&]() { - jobject fabricUIManager = fabricUIManager_.get(); + const jni::global_ref& fabricUIManager = + contextContainer_->at>("FabricUIManager"); static auto measureLines = jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") ->getMethodat>("FabricUIManager"); auto attributedStringMB = JReadableMapBuffer::createWithContents( toMapBuffer(attributedString)); auto paragraphAttributesMB = JReadableMapBuffer::createWithContents( @@ -362,12 +364,13 @@ TextLayoutManager::PreparedTextLayout TextLayoutManager::prepareLayout( javaReactTags->setRegion( 0, static_cast(reactTags.size()), reactTags.data()); - jobject fabricUIManager = fabricUIManager_.get(); + const auto& fabricUIManager = + contextContainer_->at>("FabricUIManager"); return PreparedTextLayout{ jni::make_global(reusePreparedLayoutWithNewReactTags( fabricUIManager, preparedText->get(), javaReactTags.get()))}; } else { - return *preparedText; + return PreparedTextLayout{*preparedText}; } } @@ -375,7 +378,8 @@ TextMeasurement TextLayoutManager::measurePreparedLayout( const PreparedTextLayout& preparedLayout, const TextLayoutContext& /*layoutContext*/, const LayoutConstraints& layoutConstraints) const { - jobject fabricUIManager = fabricUIManager_.get(); + const auto& fabricUIManager = + contextContainer_->at>("FabricUIManager"); static auto measurePreparedLayout = jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h index 1345772ab5c0..dc2cfcd869dd 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -120,9 +120,6 @@ class TextLayoutManager { private: std::shared_ptr contextContainer_; - // FabricUIManager, read from the context container once: reading it copies - // its JNI global reference. - const SafeReleaseJniRef> fabricUIManager_; TextMeasureCache textMeasureCache_; LineMeasureCache lineMeasureCache_; SimpleThreadSafeCache preparedTextCache_;