Skip to content

一个比较隐蔽的内存泄漏的Bug,并引发了奇怪的行为。 #512

Open
@aitsuki

Description

@aitsuki

先来看一个测试用例

/// Rebuild the widget after 'refresh' or 'loadmore' is completed, check
/// their callbaks can get a correct timestamp(defined in 'builder').
/// If get a wrong timestamp, consider memory leak.
testWidgets("test memory leak", (tester) async {
  final controller = RefreshController();
  int currentTimestamp = 0;
  await tester.pumpWidget(MaterialApp(
    home: Directionality(
      textDirection: TextDirection.ltr,
      child: Builder(
          key: Key('builder'),
          builder: (context) {
            int timestamp = DateTime.now().millisecondsSinceEpoch;
            currentTimestamp = timestamp;
            print('$timestamp ---- build');
            return SmartRefresher(
              controller: controller,
              enablePullUp: true,
              onLoading: () {
                controller.loadComplete();
                print('$timestamp --- onLoading');
                expect(timestamp, currentTimestamp);
                tester
                    .firstElement(find.byKey(Key('builder')))
                    .markNeedsBuild();
              },
              onRefresh: () {
                controller.refreshCompleted();
                print('$timestamp --- onRefresh');
                expect(timestamp, currentTimestamp);
                tester
                    .firstElement(find.byKey(Key('builder')))
                    .markNeedsBuild();
              },
              child: ListView(
                children: List<Widget>.generate(20, (index) {
                  return Container(height: 50, child: Text('Item: $index'));
                }),
              ),
            );
          }),
    ),
  ));

  // test pull refresh
  await tester.drag(find.byType(Scrollable), const Offset(0, 100.0),
      touchSlopY: 0.0);
  await tester.pumpAndSettle();
  await tester.drag(find.byType(Scrollable), const Offset(0, 100.0),
      touchSlopY: 0.0);
  await tester.pumpAndSettle();

  // test load more
  controller.position!.jumpTo(controller.position!.maxScrollExtent - 30.0);
  await tester.drag(find.byType(Scrollable), const Offset(0, -30.0));
  await tester.pumpAndSettle();
  await tester.drag(find.byType(Scrollable), const Offset(0, -30.0));
  await tester.pumpAndSettle();
});
1628278954679 ---- build
1628278954679 --- onRefresh
1628278955294 ---- build
1628278955294 --- onRefresh
1628278955405 ---- build
1628278955405 --- onLoading
1628278955452 ---- build
1628278955405 --- onLoading
══╡ EXCEPTION CAUGHT BY FOUNDATION LIBRARY ╞════════════════════════════════════════════════════════
The following TestFailure object was thrown while dispatching notifications for
RefreshNotifier<LoadStatus>:
  Expected: <1628278955452>
  Actual: <1628278955405>

在加载更多的onLoading回调中,打印的时间戳是永远是首次回调onLoading得到的那个,初步考虑是内存泄漏了,onLoading持有了应该要被销毁的Builder。通过devtools检查,发现确实存在两个SmartRefresher实例,gc无法回收。

初步判断问题可能出在这里,因为我没有设置header和footer,使用的是默认的:

class SmartRefresherState extends State<SmartRefresher> {

  final RefreshIndicator defaultHeader =
      defaultTargetPlatform == TargetPlatform.iOS
          ? ClassicHeader()
          : MaterialClassicHeader();

  final LoadIndicator defaultFooter = ClassicFooter();
}

这里header实例和footer实例保存在了State中,所以父节点重建时,这两个widget不会被重建,而且他们应该还持有了SmartRefresher对象但是没有被正确释放。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions