flutter 富文本渲染異常

起因

bugly捕捉到大量系統(tǒng)異常,所有方法都在render層,無法定位問題代碼。

  1. 異常1
1   [uid: 10423443 - uuid: fe027965f990c232] #0 RenderBox.size (package:flutter/src/rendering/box.dart:1962)
2   #1 RenderPadding.performLayout (package:flutter/src/rendering/shifted_box.dart:230)
3   #2 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
4   #3 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:118)
5   #4 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
6   #5 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:118)
7   #6 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
8   #7 RenderSliverMultiBoxAdaptor.insertAndLayoutChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:487)
9   #8 RenderSliverList.performLayout.advance (package:flutter/src/rendering/sliver_list.dart:239)
10  #9 RenderSliverList.performLayout (package:flutter/src/rendering/sliver_list.dart:282)
11  #10 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
12  #11 RenderSliverEdgeInsetsPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:137)
13  #12 RenderSliverPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:371)
14  #13 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
15  #14 RenderViewportBase.layoutChildSequence (package:flutter/src/rendering/viewport.dart:512)
16  #15 RenderViewport._attemptLayout (package:flutter/src/rendering/viewport.dart:1570)
17  #16 RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1479)
18  #17 RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:1641)
19  #18 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:884)
20  #19 RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:453)
  1. 異常2
1   [uid: 10423443 - uuid: fe027965f990c232] #0 RenderBox.size (package:flutter/src/rendering/box.dart:1962)
2   #1 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:119)
3   #2 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
4   #3 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:118)
5   #4 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
6   #5 RenderSliverMultiBoxAdaptor.insertAndLayoutChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:487)
7   #6 RenderSliverList.performLayout.advance (package:flutter/src/rendering/sliver_list.dart:239)
8   #7 RenderSliverList.performLayout (package:flutter/src/rendering/sliver_list.dart:282)
9   #8 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
10  #9 RenderSliverEdgeInsetsPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:137)
11  #10 RenderSliverPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:371)
12  #11 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
13  #12 RenderViewportBase.layoutChildSequence (package:flutter/src/rendering/viewport.dart:512)
14  #13 RenderViewport._attemptLayout (package:flutter/src/rendering/viewport.dart:1570)
15  #14 RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1479)
16  #15 RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:1641)
17  #16 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:884)
18  #17 RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:453)
19  #18 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:874)
20  #19 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:319)
  1. 異常3

1   [uid: 10423443 - uuid: fe027965f990c232] #0 RenderBox.size (package:flutter/src/rendering/box.dart:1962)
2   #1 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:119)
3   #2 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
4   #3 RenderSliverMultiBoxAdaptor.insertAndLayoutChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:487)
5   #4 RenderSliverList.performLayout.advance (package:flutter/src/rendering/sliver_list.dart:239)
6   #5 RenderSliverList.performLayout (package:flutter/src/rendering/sliver_list.dart:282)
7   #6 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
8   #7 RenderSliverEdgeInsetsPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:137)
9   #8 RenderSliverPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:371)
10  #9 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
11  #10 RenderViewportBase.layoutChildSequence (package:flutter/src/rendering/viewport.dart:512)
12  #11 RenderViewport._attemptLayout (package:flutter/src/rendering/viewport.dart:1570)
13  #12 RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1479)
14  #13 RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:1641)
15  #14 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:884)
16  #15 RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:453)
17  #16 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:874)
18  #17 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:319)
19  #18 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1144)
20  #19 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1082)
  1. 異常4
1   [uid: 10423443 - uuid: fe027965f990c232] #0 ParagraphBuilder.addText (dart:ui/text.dart:2178)
2   #1 TextSpan.build (package:flutter/src/painting/text_span.dart:204)
3   #2 TextSpan.build (package:flutter/src/painting/text_span.dart:208)
4   #3 TextSpan.build (package:flutter/src/painting/text_span.dart:208)
5   #4 TextPainter.layout (package:flutter/src/painting/text_painter.dart:569)
6   #5 RenderParagraph._layoutText (package:flutter/src/rendering/paragraph.dart:515)
7   #6 RenderParagraph._layoutTextWithConstraints (package:flutter/src/rendering/paragraph.dart:538)
8   #7 RenderParagraph.performLayout (package:flutter/src/rendering/paragraph.dart:651)
9   #8 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
10  #9 RenderPositionedBox.performLayout (package:flutter/src/rendering/shifted_box.dart:430)
11  #10 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
12  #11 ChildLayoutHelper.layoutChild (package:flutter/src/rendering/layout_helper.dart:54)
13  #12 RenderFlex._computeSizes (package:flutter/src/rendering/flex.dart:897)
14  #13 RenderFlex.performLayout (package:flutter/src/rendering/flex.dart:932)
15  #14 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
16  #15 RenderPadding.performLayout (package:flutter/src/rendering/shifted_box.dart:226)
17  #16 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
18  #17 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:118)
19  #18 RenderObject.layout (package:flutter/src/rendering/object.dart:1784)
20  #19 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:118)

排查

異常的堆棧都在renderObject層,無法定位哪行代碼出的問題。

異常捕獲:為什么有的在build層能定位,為什么有的在renderObject層?

performRebuild

在performRebuild中調(diào)用build()和updateChild()方法。在StatelessElement和StatefulElement中通過Widget build() => state.build(this);調(diào)用到自定義widget中的build,其中build發(fā)生異常,被try...catch捕捉到堆棧,定位到具體的代碼行數(shù)。

//framework.dart
abstract class ComponentElement extends Element {
  @override
  void performRebuild() {
    ...
    Widget? built;
  
    try {
      ...
      built = build();
      ...
    } catch (e, stack) {
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () sync* {
            yield DiagnosticsDebugCreator(DebugCreator(this));
          },
        ),
      );
    } finally {
      _dirty = false;
    }
    try {
      _child = updateChild(_child, built, slot);
    } catch (e, stack) {
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () sync* {
            yield DiagnosticsDebugCreator(DebugCreator(this));
          },
        ),
      );
      _child = updateChild(null, built, slot);
    }
    ...
  }
}

performLayout

performLayout在RenderObject層調(diào)用,用try...catch捕捉。RenderObejct是由element管理,在framework層面,不經(jīng)過開發(fā)者。捕捉到的堆棧在renderObejct層。

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
  void _layoutWithoutResize() {
    try {
      performLayout();
      markNeedsSemanticsUpdate();
    } catch (e, stack) {
      _debugReportException('performLayout', e, stack);
    }
  
    _needsLayout = false;
    markNeedsPaint();
  }
}

小結(jié):flutter異常多數(shù)都是null value,例如build方法中出現(xiàn)數(shù)組越界,空異常等;performLayout中的size方法獲取用了!等。

突破口\ud83d

  1. 在找Invalid argument(s): string is not well-formed UTF-16時查找到https://bugs.chromium.org/p/skia/issues/detail?id=12850。大概意思是:Text.rich渲染\ud83d會拋異常,測試代碼如下:
TextSpan(
  text: "\ud83d",
  style: TextStyle(color: e.color, fontSize: 14,),
)

探索\ud83d

  1. \ud83d是什么?只知道是unicode編碼,什么情況下會出現(xiàn)這個字符?

\ud83d是unicode的一個碼。特殊字符(包含emoji)的梳理,\ud83d是表情組成部分。
一個表情是由兩個unicode碼組成的,如??的unicode碼就是\uD83D\uDDA4。

  1. 會不會是平臺兼容性問題?如iOS的表情到Android導(dǎo)致的。
  2. 會不會是第三方輸入法導(dǎo)致?搜狗、科大訊飛、百度等。

以上問題經(jīng)過測試,表情都是以兩個unicode碼成對出現(xiàn)的,不會出現(xiàn)問題。答案就在眼前,但不知道原因。查看退貨查驗代碼,有兩處使用Text.rich。一個是商品cell,一個是備注。

與后端溝通

考慮與后端溝通,根據(jù)bugly時間獲取前后的時間的返回報文。
第一次:沒有復(fù)現(xiàn)。
第二次思考信息更全面,同時也意識到不足。如掃碼時是否能帶上code碼,方便排查。后期有userId,比較好過濾。同時對后端的過濾規(guī)則有一定了解。
這次把detail和keyword接口都查到了。在最新的代碼沒有復(fù)現(xiàn)。想到切到問題版本,問題必現(xiàn)了。
進入退貨查驗,滑到底部,開始出現(xiàn)大量異常。問題定位在備注,與上面的表情分析不謀而合。

原因

查看到TextSpan中的model中有×的符號。
查找來源,在高亮?xí)r發(fā)現(xiàn)了如下代碼:

//2022-06-23 16:02:28 - 王太松200201 - ?? - 1
if (isKeywordNotEmpty && isRemarkNotEmpty) {
  // 展開, 分割, 去重, toList, join
  final k = words.join().split('').toSet().toList().join();
  bloc.model?.remarkList?.forEach((e) {
    final remark = e.content.split('').map<KeywordItem>((e) {
      return KeywordItem(e, k.contains(e) ? Colors.red : Colors.black);
    }).toList();
    bloc.remarks.add(remark);
  });
}

split('')會把一個表情切成兩個unicode,而單獨unicode被TextSpan渲染就出現(xiàn)異常。

總結(jié)

  1. 對異常捕獲原理有很好的分析。
  2. 根據(jù)異常去看源碼,有重點。
  3. 對于renderObject層報出的異常,找到一種新的解決方式。基本對flutter渲染有了全面的了解。
  4. 對于線上數(shù)據(jù),客戶端是無感知的,排查麻煩。與后端溝通是個很好的復(fù)現(xiàn)場景的方式。
  5. 對flutter渲染表情異常有深刻的認(rèn)識。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容