目录
Jing Huang

Reset leaked StaticLayout bounds-for-width flags on Android 15 (gated)

Summary:

Problem

45 of 47 FB4A Flytrap reports in https://fburl.com/scuba/flytrap_facebook_canonical/3sm0dpk4 describe the same bug: after tapping “See more” on a long post, the text slides off the right edge and only a letter or two per line stays visible. In a sample of incorrect_ui reports, 68 of 300 on 582 / Android 15 are this bug. There were 0 of 300 on 582 / Android 16 and about 0 of 200 on 581 / Android 15. It still happens on 583. Almost all the posts are Hebrew, Arabic, or Urdu with several paragraphs.

Root cause, confirmed in a live app process on an Android 15 emulator with live Kotlin eval:

  • StaticLayout.Builder.obtain() takes builders from a static pool of 3. On Android 15 it does not reset useBoundsForWidth (or shiftDrawingOffsetForStartOverhang) when it hands one out. A TextView in an app targeting SDK 35+ builds with useBoundsForWidth = true, so the next pooled builder still has it. A builder freshly obtained in the bad process read mUseBoundsForWidth = true.
  • TextLayoutBuilder (the layout engine behind Litho Text, and so behind feed post text) never sets these flags. Its layout inherits whatever the pooled builder had.
  • With useBoundsForWidth = true, a multi-paragraph RTL text with empty lines gets mDrawingBounds.left = -652.8 at width 672. Layout.draw() then shifts the canvas by -left, about 653 px to the right. The Litho layout tree, the TextDrawable bounds, and the translation are all correct; only the framework draw is offset.
  • It is intermittent because it depends on which pooled builder the layout gets and whether a TextView used it last. That matches “doesn’t happen all the time” in the reports. A fresh StaticLayout built from the same text with a fresh paint draws offset in the bad process. The same text with either flag set to false draws correctly.
BEFORE (Android 15, gate off)
  TextView (target >= 35) --build(useBoundsForWidth=true)--> [Builder pool x3]   (obtain() does not reset it)
                                                                  |
  Litho Text -> TextLayoutBuilder -> StaticLayoutHelper.make -> obtain() -> builder still has useBoundsForWidth=true
                                                                  |
  multi-paragraph RTL text -> drawingBounds.left = -653 -> Layout.draw() translates +653 px
                                                                  |
  screen:  |                                         ...ט|   <- only the line ends are visible
           |                                          ...ה|

AFTER (gate on)
  StaticLayoutHelper.make on API 35+:
     builder.setUseBoundsForWidth(false).setShiftDrawingOffsetForStartOverhang(false)
                                                                  |
  drawingBounds unused -> text drawn inside the content box (x = 24..696)
  screen:  |בדיקה שלישית עם הקונפיגורציה של המדווח, פוסט|

Diagram (Excalidraw): https://pxl.cl/gr3vG

Fix

StaticLayoutHelper.make() now sets both flags to false explicitly on API 35+. False is the value TextLayoutBuilder has always assumed, so this only undoes the leak. The switch is a new process-wide TextLayoutBuilderConfiguration.resetStaticLayoutBoundsForWidth (default false, so other apps that use the library are unchanged). FB4A sets it at startup from MobileConfig in FbLithoStartupConfiguration, next to the other Litho text flags.

The fix is in TextLayoutBuilder, not in feed or FDS code, because the corruption reaches every Litho Text and primitive text that measures through TextLayoutBuilder. Feed post text is where it shows most, because long multi-paragraph RTL posts are what produce the large negative drawing bounds.

Knobs

Knob Type / scope Default What it does Recommended
litho_core.reset_static_layout_bounds_for_width (key 158) bool, ADMIN_ID, read once at app startup false When true, every StaticLayout that TextLayoutBuilder builds on API 35+ has useBoundsForWidth and shiftDrawingOffsetForStartOverhang set to false, so the flags leaked by Android 15’s builder pool cannot reach Litho text. When false, behavior is unchanged. true. Ramp with an A/B on FB4A and watch the Android 15 “See more” text reports in Flytrap plus text-heavy perf metrics. Expect a no-op on Android 14 and below (the code path is API 35+ only). On Android 16+ it should also be a no-op, assuming the framework already resets the flags there.

MC actualization: https://fburl.com/metaconfig/he3ftgyv

Not in this diff

  • BkBloksTextSpanStyleResizeToFitStyleProvider calls StaticLayout.Builder.obtain directly and can inherit the same flags. It is outside TextLayoutBuilder; follow-up if Bloks text shows the same symptom.
  • The pool not resetting these flags is a framework bug. The 0-of-300 result on Android 16 suggests it is fixed there; not confirmed on a device.

Differential Revision: D126788375

fbshipit-source-id: 07051d9061163f8caf12a6db792ee8bbaec2ee35

6天前145次提交

TextLayoutBuilder

Build text Layouts easily on Android.

Features

  • Create text Layouts easily.
  • Reuse builders to create similarly styled Layouts.
  • Cache Layouts of commonly used strings.
  • Improve performance by warming up the FreeType cache.

Download

If using Gradle, add this to your build.gradle:

compile 'com.facebook.fbui.textlayoutbuilder:textlayoutbuilder:1.7.0'

or, if using Maven:

<dependency>
  <groupId>com.facebook.fbui.textlayoutbuilder</groupId>
  <artifactId>textlayoutbuilder</artifactId>
  <version>1.7.0</version>
  <type>aar</type>
</dependency>

Usage

  1. Set the properties on the TextLayoutBuilder:

    TextLayoutBuilder builder = new TextLayoutBuilder()
       .setText("TextLayoutBuilder makes life easy")
       .setTextColor(Color.BLUE)
       .setWidth(400 /*, MEASURE_MODE_EXACTLY */);
  2. Call build() on the builder to get a Layout:

    Layout layout = builder.build();
  3. Use the Layout in your code:

    public class CustomView extends View {
       private Layout layout;
    
       public CustomView(Context context) {
           super(context);
       }
    
       public void setLayout(Layout layout) {
           this.layout = layout;
       }
    
       @Override
       protected void onDraw(Canvas canvas) {
           super.onDraw(canvas);
    
           // Draw the layout.
           layout.draw(canvas);
       }
    }

Additional Usage

  1. Cache the layouts for commonly used strings by turning on caching in the TextLayoutBuilder.

    textLayoutBuilder.setShouldCacheLayout(true);
  2. Glyph warming provides significant performance boost for large blurbs of text. Turn this on and pass in a GlyphWarmer for the TextLayoutBuilder.

    textLayoutBuilder
       .setShouldWarmText(true)
       .setGlyphWarmer(new GlyphWarmerImpl());
  3. Import a style defined in XML into a TextLayoutBuilder object.

    ResourceTextLayoutHelper.updateFromStyleResource(
       textLayoutBuilder, // builder object
       context,           // Activity context
       resId);            // style resource id

License

TextLayoutBuilder is Apache-2-licensed.

关于
788.0 KB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9 京公网安备 11010802047560号