Skip to content

fix: borrow list ANR from infinite-height grid measurement - #176

Open
A1nair wants to merge 11 commits into
BenderBlog:mainfrom
A1nair:fix/borrow-list-anr
Open

fix: borrow list ANR from infinite-height grid measurement#176
A1nair wants to merge 11 commits into
BenderBlog:mainfrom
A1nair:fix/borrow-list-anr

Conversation

@A1nair

@A1nair A1nair commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

问题

起因

在一加Ace2上点击图书馆信息之后卡死闪退

使用 adb logcat 获取原因

ApplicationExitInfo ... reason=6 (ANR) ...
description=user request after error:
Input dispatching timed out (
  io.github.benderblog.traintime_pda/...MainActivity is not responding.
  Waited 5000ms for MotionEvent(..., action=DOWN, ...)

Input dispatching timed out ... Waited 5000ms 说明点击后主线程被阻塞超过 5 秒,不是普通 Dart 异常崩溃,而是主 isolate 陷入同步死循环 / 无限布局DartWorker 后台线程当时占用 ~6.2% CPU,但主 UI isolate 卡死。

布局循环

#304  _LayoutBuilderElement._rebuildWithConstraints.updateChildCallback
       (package:flutter/src/widgets/layout_builder.dart:247)
#306  _LayoutBuilderElement._rebuildWithConstraints
       (package:flutter/src/widgets/layout_builder.dart:270)
#307  RenderAbstractLayoutBuilderMixin.layoutCallback
       (package:flutter/src/widgets/layout_builder.dart:333)
...
#313  _RenderLayoutBuilder.performLayout
       (package:flutter/src/widgets/layout_builder.dart:447)
#315  RenderProxyBoxMixin.performLayout
       (package:flutter/src/rendering/proxy_box.dart:118)
#317  RenderProxyBoxMixin.performLayout   ← 上述两段交替出现 300+ 层
...
#350  _RenderTheater.performLayout        (Overlay / 路由树根)

LayoutBuilder 无限布局循环:外层 _RenderLayoutBuilder(来自 BorrowListViewLayoutBuilder)与内层 _LayoutBuilderElement(来自 AlignedGridView 内部的 SliverLayoutBuilder)互相触发布局,Flutter 的布局深度保护触发 Layout cycle。

去掉外层 LayoutBuilder 后变成 OOM

BorrowListViewLayoutBuilder 临时去掉后,profile 日志由"布局循环"变为:

E/DartVM  (23541): Exhausted heap space, trying to allocate 228... bytes.
E/DartVM  (23541): Exhausted heap space, trying to allocate 228... bytes.
...(持续)

这证明循环发生在测量/分配阶段(不是 build 阶段),且是无限分配——与"网格对每张卡片反复测量"的假设一致。

为什么卡片会被测成无限高

flutter_staggered_grid_view-0.7.0 源码:

SliverAlignedGrid.buildsliver_aligned_grid.dart:132

return SliverLayoutBuilder(
  builder: (context, constraints) {
    final crossAxisCount = gridDelegate.getCrossAxisCount(constraints, crossAxisSpacing);
    ...
    return SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) {
          ...
          return UniformTrack(                 // 一行卡片
            direction: constraints.crossAxisDirection,
            division: crossAxisCount,
            spacing: crossAxisSpacing,
            children: [...children],
          );
        },
        childCount: listItemCount,
      ),
    );
  },
);

每行用 UniformTrack 把一行里所有卡片拉伸到最宽那张的高度。

RenderUniformTrack._computeSizeuniform_track.dart:155-180

Size _computeSize(BoxConstraints constraints, ChildLayouter layoutChild) {
  final mainAxisExtent = _isHorizontal ? constraints.maxWidth : constraints.maxHeight;
  final childMainAxisExtent = ((mainAxisExtent + spacing) / division) - spacing;
  final childConstraints = _isHorizontal
      ? BoxConstraints.tightFor(width: childMainAxisExtent)
      : BoxConstraints.tightFor(height: childMainAxisExtent);   // ← 纵向滚动走这里
  RenderBox? child = firstChild;
  ...
  while (child != null) {
    final size = layoutChild(child, childConstraints);          // ← 用 childConstraints 测量每张卡
    maxChildCrossAxisExtent = math.max(maxChildCrossAxisExtent, getChildCrossAxisExtent(size));
    child = childAfter(child);
  }
  ...
}

关键点:UniformTrackSliverList 的子项。在滚动 sliver 中,子 box 收到的主轴(纵向)约束 maxHeight = double.infinity(sliver 主轴高度无界,子项高度由其自身内容决定)。于是:

  • mainAxisExtent = constraints.maxHeight = double.infinity
  • childMainAxisExtent = ((infinity + spacing) / division) - spacing = double.infinity
  • 每张 BorrowInfoCard 被以 BoxConstraints.tightFor(height: double.infinity) 布局,即 minHeight = maxHeight = 无限

修复

lib/page/library/borrow_info_card.dart

把卡片内容的最外层 RowSizedBox(height: 140) 包裹:

return SizedBox(
  height: 140,
  child: [
    CachedNetworkImage(...).clipRRect(all: 14),
    const VerticalDivider(),
    [ ... 文字列 ... ].toColumn(...).expanded(),
  ].toRow(),
).padding(horizontal: 12, vertical: 4).card(elevation: 0);

AlignedGridView 内部 UniformTrack 会以 tightFor(height: infinity) 测量每张卡;BorrowInfoCard 顶层的 .expanded() 会把卡片撑成无限高,导致整张网格测量不收敛。用 SizedBox(height: 140) 给卡片一个有限且确定的高度,覆盖传入的无限高约束,使网格测量一次收敛。

140 为 250 * 0.5 留有余量。

lib/page/library/borrow_list_view.dart

去掉外层的 LayoutBuilder,改用一次性计算的 MediaQuery 求列数,并增加最小值约束:

@override
Widget build(BuildContext context) {
  final crossAxisCount = max(1, MediaQuery.of(context).size.width ~/ 360);
  return Scaffold(
    body: Stack(
      children: <Widget>[
        if (borrowList.isEmpty) EmptyListView(...),
        AlignedGridView.count(
          ...
          crossAxisCount: crossAxisCount,
          ...
        ),
      ],
    ),
    bottomNavigationBar: ...,
  );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants