Flutter 内存泄漏 - Flutter Bloc

War*_*sen 4 memory-leaks dart flutter bloc

我正在使用 Flutter 构建一个清单应用程序,我似乎正在跨检查点和检查建立内存。我已经来来回回 2 周了,现在试图重组页面无济于事。

我正在使用 Flutter Bloc https://felangel.github.io/bloc在检查点屏幕上进行状态管理。我怀疑 Bloc 导致了我的内存泄漏。

检查点屏幕非常复杂:

  1. 显示用户在清单中的位置的标题,即。检查点 4/50。
  2. 显示答案选项的小部件:好的,有缺陷的,不适用。
  3. 一个小部件,允许用户为检查点拍摄最多 2 张图像
  4. 用于输入检查点结果的 TextFormField 小部件

当用户提交检查点时,将存储答案并为用户显示下一个检查点,直到他们到达检查结束并关闭它。

这是屏幕的屏幕截图,不幸的是这里无法看到 TextFormField,但它就在“发现”这个词的下方。

检查点屏幕截图

我注意到的事情:

当检查点屏幕第一次加载并且我在 DevTools 中拍摄快照时,我可以看到每个小部件的 1 个实例(AnswerOptions、CheckHeader、Comments、ImageGrid)。但是,一旦我开始切换选项,即。在 OK、DEFECTIVE、N/A 之间跳跃,实例(AnswerOptions、CheckHeader、Comments、ImageGrid)开始堆积。当用户提交检查点甚至完全退出检查时,这些类会留在内存堆中并且永远不会被释放。

我还注意到重复的实例仅从 CheckpointForm 向下通过小部件树开始。AssetInspection 和 InspectionView 不会在堆中复制实例。

页面首次加载时的示例:

1 CheckHeader 实例

然后我切换 OK、DEFECTIVE 和 N/A 并拍摄另一个快照:

切换选项后实例已累积

附上代码:

资产检查

class AssetInspection extends StatefulWidget
{
  final Checklist checklist;
  final Asset asset;
  final Job job;
  final AssetPoolDatabase database;

  AssetInspection({
    Key key,
    @required this.checklist,
    @required this.asset,
    @required this.job,
    @required this.database,
  }) : super(key: key);

  @override
  AssetInspectionState createState() => new AssetInspectionState();
}

class AssetInspectionState extends State<AssetInspection>
{
  InspectionBloc _inspectionBloc;
  CheckpointBloc _checkpointBloc;

  @override
  void initState() {
    _checkpointBloc = CheckpointBloc(
      database: widget.database,
      answerRepo: AnswerRepo(database: widget.database),
    );

    _inspectionBloc = InspectionBloc(
      checklist: widget.checklist,
      job: widget.job,
      asset: widget.asset,
      inspectionRepo: InspectionRepo(database: widget.database),
      checkpointBloc: _checkpointBloc
    );
    super.initState();
  }

  @override
  void dispose() {
    _inspectionBloc.dispose();
    _checkpointBloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return MultiBlocProvider(
      providers: [
        BlocProvider<InspectionBloc>(
          builder: (BuildContext context) => _inspectionBloc..dispatch(LoadInspection()),
        ),
        BlocProvider<CheckpointBloc>(
          builder: (BuildContext context) => _checkpointBloc,
        )
      ],
      child: InspectionView(),
    );
  }

}
Run Code Online (Sandbox Code Playgroud)

检查视图

class InspectionView extends StatelessWidget
{

  @override
  Widget build(BuildContext context) {
    final InspectionBloc _inspectionBloc = BlocProvider.of<InspectionBloc>(context);

    return BlocListener(
      bloc: _inspectionBloc,
      listener: (context, InspectionState state) {
        if(state is AnswerStored) {
          _inspectionBloc..dispatch(LoadInspection());
        }

        if(state is InspectionClosed) {
          Navigator.pushReplacement(
            context,
            CupertinoPageRoute(
              builder: (context) => JobManager(
                jobId: state.inspection.jobId,
              ),
            ),
          );
        }
      },
      child: BlocBuilder<InspectionBloc, InspectionState>(
        builder: (BuildContext context, InspectionState state) {
          if (state is InspectionInProgress) {
            return CheckpointView(
              currentCheck: state.currentCheck,
              totalChecks: state.totalChecks,
            );
          }

          if(state is InspectionNeedsSubmission) {
            return SubmitInspection(
              inspection: state.inspection,
              checklist: state.checklist,
            );
          }

          if(state is InspectionLoading) {
            return LoadingIndicator();
          }

          return LoadingIndicator();
        },
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

检查点视图

class CheckpointView extends StatelessWidget {

  final int totalChecks;
  final int currentCheck;

  CheckpointView({
    Key key,
    @required this.totalChecks,
    @required this.currentCheck,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CheckpointBloc, CheckpointState>(
      builder: (context, CheckpointState state) {
        if(state is CheckpointLoaded) {
          return CheckpointForm(
            totalChecks: totalChecks,
            currentCheck: currentCheck,
          );
        }

        if(state is ManagingImage) {
          return ImageOptions();
        }

        return Container(color: Colors.white,);
      },
    );
  }

}
Run Code Online (Sandbox Code Playgroud)

检查点表格

class CheckpointForm extends StatelessWidget
{
  final int totalChecks;
  final int currentCheck;

  CheckpointForm({
    this.totalChecks,
    this.currentCheck,
    Key key
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {

    final InspectionBloc _inspectionBloc = BlocProvider.of<InspectionBloc>(context);
    final CheckpointBloc _checkpointBloc = BlocProvider.of<CheckpointBloc>(context);

    final CheckpointLoaded currentState = _checkpointBloc.currentState as CheckpointLoaded;

    return Scaffold(
      appBar: AppBar(
        title: Text(_inspectionBloc.checklist.name),
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: () {
            Navigator.pushReplacement(
              context,
              CupertinoPageRoute(
                builder: (context) => JobManager(
                  jobId: _inspectionBloc.job.id,
                ),
              ),
            );
          },
        ),
      ),
      body: GestureDetector(
        onTap: () {
          FocusScope.of(context).requestFocus(new FocusNode());
        },
        child: SingleChildScrollView(
          padding: const EdgeInsets.only(left: 15, right: 15, top: 20, bottom: 20),
          child: Column(
            children: <Widget>[
              CheckHeader(
                totalChecks: totalChecks,
                currentCheck: currentCheck,
              ),
              AnswerOptions(),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  const Text('Evidence',
                    style: const TextStyle(
                      fontSize: 20, fontWeight: FontWeight.w600)
                    ),
                  Padding(
                    padding: const EdgeInsets.only(left: 10),
                    child: Text(
                      _getImageValidationText(currentState),
                      style: const TextStyle(
                        color: Colors.deepOrange,
                        fontWeight: FontWeight.w500
                      ),
                    ),
                  )
                ],
              ),
              const Divider(),
              ImageGrid(),
              CheckpointComments(),
            ],
          ),
        ),
      ),
    );
  }

  String _getImageValidationText(CheckpointLoaded state) {
    if ((state.checkpoint.imageRule == 'when-defective' &&
            state.answer.answer == '0' &&
            state.answer.images.length == 0) ||
        (state.checkpoint.imageRule == 'always-required' &&
            state.answer.images.length == 0)) {
      return 'Please take up to 2 images';
    }
    return '';
  }
}
Run Code Online (Sandbox Code Playgroud)

检查头

class CheckHeader extends StatelessWidget
{
  final int totalChecks;
  final int currentCheck;

  CheckHeader({
    Key key,
    @required this.totalChecks,
    @required this.currentCheck,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {

    final CheckpointBloc _checkpointBloc = BlocProvider.of<CheckpointBloc>(context);

    return BlocBuilder(
      bloc: _checkpointBloc,
      builder: (context, CheckpointState state) {
        if(state is CheckpointLoaded) {
          return Container(
            padding: const EdgeInsets.only(top: 20, bottom: 20),
            margin: const EdgeInsets.only(bottom: 30),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text('Check: $currentCheck/$totalChecks'),
                Text(
                  state.checkpoint.name,
                  style: const TextStyle(
                    fontSize: 25,
                    fontWeight: FontWeight.w900
                  ),
                ),
                const Divider(),
                Text(
                  state.checkpoint.task,
                  style: const TextStyle(
                    fontSize: 18
                  ),
                )
              ],
            ),
          );
        }

        return Container(color: Colors.white,);
      },
    );
  }
}

Run Code Online (Sandbox Code Playgroud)

答案选项

class AnswerOptions extends StatelessWidget
{
  AnswerOptions({
    Key key
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {

    final CheckpointBloc _checkpointBloc = BlocProvider.of<CheckpointBloc>(context);

    CheckpointLoaded state = _checkpointBloc.currentState as CheckpointLoaded;

    return Column(
      children: <Widget>[
        _option(
          label: 'Pass Check',
          value: '1',
          activeValue: state.answer.answer,
          activeColor: AssetPoolTheme.green,
          activeTextColor: Colors.white,
          passiveTextColor: Colors.blueGrey,
          passiveColor: AssetPoolTheme.grey,
          icon: Icons.check_circle_outline,
          state: state,
          checkpointBloc: _checkpointBloc
        ),
        _option(
          icon: Icons.highlight_off,
          label: 'Fail Check',
          value: '0',
          activeValue: state.answer.answer,
          activeColor: AssetPoolTheme.red,
          activeTextColor: Colors.white,
          passiveTextColor: Colors.blueGrey,
          passiveColor: AssetPoolTheme.grey,
          state: state,
          checkpointBloc: _checkpointBloc
        ),
        _option(
          icon: Icons.not_interested,
          label: 'Not Applicable',
          value: '-1',
          activeValue: state.answer.answer,
          activeTextColor: Colors.white,
          passiveTextColor: Colors.blueGrey,
          passiveColor: AssetPoolTheme.grey,
          activeColor: AssetPoolTheme.orange,
          state: state,
          checkpointBloc: _checkpointBloc
        ),
      ],
    );
  }

  _option({ 
    icon, 
    label, 
    value, 
    activeValue, 
    activeTextColor, 
    passiveTextColor, 
    passiveColor, 
    activeColor, 
    state, 
    checkpointBloc
    }) {
    return Container(
      margin: const EdgeInsets.only(bottom: 10),
      child: FlatButton(
        color: activeValue == value ? activeColor : passiveColor,
        textColor: Colors.white,
        disabledColor: Colors.grey,
        disabledTextColor: Colors.black,
        padding: const EdgeInsets.all(20),
        splashColor: activeColor,
        onPressed: () {
          checkpointBloc.dispatch(
            UpdateAnswer(answer: state.answer.copyWith(answer: value))
          );
        },
        child: Row(
          children: <Widget>[
            Padding(
              child: Icon(
                icon,
                color: activeValue == value ? activeTextColor : passiveTextColor,
              ),
              padding: const EdgeInsets.only(right: 15),
            ),
            Text(
              label,
              style: TextStyle(color: activeValue == value ? activeTextColor : passiveTextColor, fontSize: 20),
            )
          ],
        ),
      ),
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

图像网格

class ImageGrid extends StatelessWidget
{

  ImageGrid({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {

    return BlocBuilder<CheckpointBloc, CheckpointState>(
      builder: (BuildContext context, CheckpointState state) {
        if(state is CheckpointLoaded) {
          return GridView.count(
            addAutomaticKeepAlives: false,
            shrinkWrap: true,
            physics: const ScrollPhysics(),
            crossAxisCount: 2,
            childAspectRatio: 1.0,
            mainAxisSpacing: 4.0,
            crossAxisSpacing: 4.0,
            children: _imagesRow(state.answer.images),
          );
        }
        return Container();
      },
    );
  }

  List<Widget> _imagesRow(stateImages) {

    final List<Widget> previewImages = [];

    stateImages.forEach((imagePath) {
      final preview =  new ImagePreview(
        key: Key(imagePath),
        imagePath: '$imagePath',
        imageName: imagePath
      );
      previewImages.add(preview,);
    });

    final takePicture = TakePicture();

    if (stateImages.length < 2) previewImages.add(takePicture,);

    return previewImages;
  }
}

Run Code Online (Sandbox Code Playgroud)

检查组

class InspectionBloc extends Bloc<InspectionEvents, InspectionState>
{
  final Checklist checklist;
  final Job job;
  final Asset asset;
  final InspectionRepo inspectionRepo;
  final CheckpointBloc checkpointBloc;

  InspectionBloc({
    @required this.checklist,
    @required this.job,
    @required this.asset,
    @required this.inspectionRepo,
    @required this.checkpointBloc,
  });

  @override
  InspectionState get initialState => InspectionUnintialized();

  @override
  void dispose() {
    checkpointBloc.dispose();
    super.dispose();
  }

  @override
  Stream<InspectionState> mapEventToState(InspectionEvents event) async* {

    if(event is LoadInspection) {

      yield InspectionLoading();
      await Future.delayed(Duration(seconds: 1));

      final Inspection inspection = await initializeInspection();
      if(inspection == null) {
        yield InspectionNotLoaded();
      } else if(inspection.syncedAt != null) {
        yield InspectionSynced(inspection: inspection);
      } else if(inspection.completedAt != null) {
        yield InspectionSynced(inspection: inspection);
      } else if(inspection.completedAt == null && inspection.syncedAt == null) {
        yield* _mapCurrentCheckpoint(inspection);
      }
    } else if(event is CheckpointWasSubmitted) {
      final bool isValid = _validateCheckpoint(event.answer, event.checkpoint);
      if(isValid == false) {
        Toaster().error('Invalid, please complete the checkpoint before submitting');
      } else {
        Inspection inspection = await inspectionRepo.getInspection(job.id, asset.localId, checklist.id);
        await _storeAnswer(event.answer, event.checkpoint, inspection);
        await inspectionRepo.jobIsInProgress(job.id);
        yield AnswerStored(
          checklist: checklist,
          asset: asset,
          job: job
        );
      }
    } else if(event is CloseInspection) {
      inspectionRepo.closeInspection(event.closingComments, event.location, event.inspection.sourceUuid);
      yield InspectionClosed(inspection: event.inspection);
    }

  }

  Stream<InspectionState> _mapCurrentCheckpoint(Inspection inspection) async* {

    final List<Check> checks = await inspectionRepo.getChecksForChecklist(checklist.id);

    if(await inspectionRepo.hasAnswers(inspection.sourceUuid) == false) {
      final Check checkpoint = await inspectionRepo.firstCheckOnChecklist(inspection.checklistId);
      yield InspectionInProgress(
        totalChecks: checks.length,
        currentCheck: 1,
        inspection: inspection,
        checkpoint: checkpoint
      );

      checkpointBloc.dispatch(LoadForInspection(checkpoint: checkpoint));

    } else {
      final Answer lastAnswer = await inspectionRepo.getLatestAnswer(inspection.sourceUuid);
      final int latestAnswerIndex = checks.indexWhere((check) => check.id == lastAnswer.checkId);
      final int updatedIndex = latestAnswerIndex + 1;
      if(updatedIndex < checks.length) {
        final Check checkpoint = checks.elementAt(updatedIndex);
        yield InspectionInProgress(
          totalChecks: checks.length,
          currentCheck: updatedIndex + 1,
          checkpoint: checkpoint,
          inspection: inspection,
        );

        checkpointBloc.dispatch(LoadForInspection(checkpoint: checkpoint));
      }

      if(updatedIndex == checks.length) {
        yield InspectionNeedsSubmission(
          inspection: inspection,
          checklist: checklist
        );
      }

    }
  }

  Future<Inspection> initializeInspection() async {
    return await inspectionRepo.getInspection(job.id, asset.localId, checklist.id) 
        ?? await inspectionRepo.createInspection(job.id, asset.localId, checklist.id);
  }

  bool _validateCheckpoint(AnswerModel answer, Check checkpoint) {
    if(answer.answer == null) return false;
    if(checkpoint.imageRule == 'always-required' && answer.images.length == 0) return false;
    if(checkpoint.commentRule == 'always-required' && answer.comments.length == 0) return false;
    if(checkpoint.imageRule == 'when-defective' && answer.answer == '0' && answer.images.length == 0) {
      return false;
    }
    if(checkpoint.commentRule == 'when-defective' && answer.answer == '0' && answer.comments.length == 0) return false;
    return true;
  }

  Future _storeAnswer(AnswerModel answerModel, Check checkpoint, Inspection inspection) async {
    inspectionRepo.storeAnswer(
      answerModel,
      checkpoint,
      inspection
    );
  }

}
Run Code Online (Sandbox Code Playgroud)

检查站集团

class CheckpointBloc extends Bloc<CheckpointEvent, CheckpointState>
{
  final AssetPoolDatabase database;
  final AnswerRepo answerRepo;

  CheckpointBloc({
    @required this.database,
    @required this.answerRepo,
  });

  @override
  CheckpointState get initialState => CheckpointNotLoaded();

  @override
  Stream<CheckpointState> mapEventToState(event) async* {
    if(event is LoadForInspection) {
      yield CheckpointLoaded(
        checkpoint: event.checkpoint,
        answer: new AnswerModel(
          checkId: event.checkpoint.id,
          images: [],
        )
      );
    } else if(event is UpdateAnswer) {
      final state = currentState as CheckpointLoaded;
      yield CheckpointLoaded(
        checkpoint: state.checkpoint,
        answer: event.answer
      );
    } else if(event is AddImage) {
      final state = currentState as CheckpointLoaded;
      List<String> images = state.answer.images;
      images.add(event.imagePath);
      yield CheckpointLoaded(
        checkpoint: state.checkpoint,
        answer: state.answer.copyWith(images: images)
      );
    } else if(event is RemoveImage) {
      print('HERE');
      print(event.imageName);
      List<String> images = event.answer.images.where((imageName) => imageName != event.imageName).toList();
      yield CheckpointLoaded(
        checkpoint: event.checkpoint,
        answer: event.answer.copyWith(images: images)
      );
    } else if(event is ManageImage) {
      yield ManagingImage(
        image: event.image,
        checkpoint: event.checkpoint,
        answer: event.answer,
        imageName: event.imageName
      );
    } else if(event is CloseImageManager) {
      yield CheckpointLoaded(
        checkpoint: event.checkpoint,
        answer: event.answer
      );
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

War*_*sen 8

我设法找到了内存泄漏。原因实际上是集团。我通过推动导航器以模态打开相机。问题是我不是从 Bloc 侦听器推送到此模式,而是从小部件内部推送。

使用 Flutter Bloc 时,建议从 Bloc 侦听器中执行导航。

我最终完全删除了导航,并简单地显示了相机小部件以响应状态的变化。

内存使用方面的变化是巨大的,垃圾收集器开始以更可预测的方式运行。