Flutter iOS 崩溃并出现 EXC_BAD_ACCESS 错误

edn*_*edn 11 dart firebase firebase-realtime-database flutter stream-builder

我正在使用 Flutter 开发一个应用程序,并在物理 iOS 设备 (iPhone 7) 上测试该应用程序。

iOS版本是:15.3.1
Flutter版本是:2.10.3

当我测试我的应用程序时,偶尔会发生崩溃。崩溃给出以下错误。它并不总是在同一个地方崩溃,所以我不知道在这里分享什么代码。错误消息本身对我来说并没有说明太多,而且我在网上找不到有关此错误的任何有用信息。

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x443b8000443b8000)
    frame #0: 0x0000000198405048 libobjc.A.dylib`objc_msgSend + 8
libobjc.A.dylib`objc_msgSend:
->  0x198405048 <+8>:  ldr    x13, [x0]
    0x19840504c <+12>: and    x16, x13, #0xffffffff8
    0x198405050 <+16>: mov    x15, x16
    0x198405054 <+20>: ldr    x11, [x16, #0x10]
Target 0: (Runner) stopped.
Lost connection to device.
Exited (sigterm)
Run Code Online (Sandbox Code Playgroud)

接下来我可以检查什么?

附录

我想知道我的 Stream Builder 是否做错了什么。这是我的代码的缩短版本:

class PrepareList extends StatefulWidget {
  final String place;
  const PrepareList({
    Key? key,
    required this.place,
  }) : super(key: key);

  @override
  State<PrepareList> createState() =>
      _PrepareListState();
}

class _PrepareListState
    extends State<PrepareList> {
  late final Stream? _listStream;

  @override
  void initState() {
    super.initState();

    String dbChild = "events/" + widget.place + "/";

    final db = FirebaseDatabase.instance
        .ref()
        .child(dbChild)
        .orderByKey()
        .limitToLast(1500);

    _listStream = db.onValue;
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
        stream: _listStream,
        builder: (context, AsyncSnapshot<dynamic> dbEvent) {
          if (dbEvent.hasError) {
            return CircularProgressIndicator();
          }
          else if (dbEvent.hasData) {
            DataSnapshot dataSnapshot = dbEvent.data!.snapshot;

            List<EventDetails> placeEventList = [];
            if (dataSnapshot.value != null) {
              (dataSnapshot.value as Map<dynamic, dynamic>)
                  .forEach((key, value) {
                placeEventList.add(EventDetails.fromRTDB(value));
              });
              placeEventList
                  .sort((a, b) => a.dateEST.compareTo(b.dateEST));

              return AttendeeList(placeEventList: placeEventList, place: place);
            } else {
              return PlaceDataNotAvailable(place: widget.place);
            }
          } else {
            return CircularProgressIndicator();
          }
        });
  }
}

class AttendeeList extends StatefulWidget {
  final List<EventDetails> placeEventList;
  final String place;
  const AttendeeList({
    Key? key,
    required this.placeEventList,
    required this.place,
  }) : super(key: key);

  @override
  State<AttendeeList> createState() =>
      _AttendeeListState();
}

class _AttendeeListState
    extends State<AttendeeList> {
  late final Stream? _attendeeListStream;

  @override
  void initState() {
    super.initState();

    String dbChild = "attendees/" + widget.place + "/";

    final db = FirebaseDatabase.instance
        .ref()
        .child(dbChild)
        .orderByKey()
        .limitToLast(1500);

    _listStream = db.onValue;
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
        stream: _attendeeListStream,
        builder: (context, AsyncSnapshot<dynamic> dbEvent) {
          if (dbEvent.hasError) {
            return CircularProgressIndicator();
          }
          else if (dbEvent.hasData) {
            DataSnapshot dataSnapshot = dbEvent.data!.snapshot;

            List<AttendeeDetails> attendeeList = [];
            if (dataSnapshot.value != null) {
              (dataSnapshot.value as Map<dynamic, dynamic>)
                  .forEach((key, value) {
                attendeeList.add(EventDetails.fromRTDB(value));
              });
              attendeeList
                  .sort((a, b) => a.dateEST.compareTo(b.dateEST));

              return Scaffold(
                body: ShowLists(placeEventList, attendeeList);
            } else {
              return PlaceDataNotAvailable(place: widget.place);
            }
          } else {
            return CircularProgressIndicator();
          }
        });
  }
}
Run Code Online (Sandbox Code Playgroud)

上面的小部件可以在应用程序的生命周期中多次调用。用户通过place在初始屏幕上选择 进入此屏幕,该屏幕执行有状态小部件中的代码PrepareList,而有状态小部件又调用AttendeeList有状态小部件。

我想强调的是,两者PrepareListAttendeeList使用流。每次执行这些小部件中的代码时,都会从数据库下载大量节点(每个小部件 1500 个)。

一次执行可能如下所示:

PrepareList("London");
Run Code Online (Sandbox Code Playgroud)

另一个执行可能如下所示,在同一屏幕上显示新的项目列表:

PrepareList("Manhattan");
Run Code Online (Sandbox Code Playgroud)

我观察到的是:

当我PrepareList("London");第一次运行时,需要一些时间(3到4秒)才能看到屏幕上的内容。然后我运行PrepareList("Manhattan");,这也需要大约 3 到 4 秒的时间来显示内容。但是当我PrepareList("London");再次运行时,内容很快就会出现在屏幕上,大约 1 秒。

为了能够调用PrepareList(),我需要转到另一个屏幕,这意味着 - 根据我的理解 -每次我离开与上面 2 个小部件关联的屏幕时,我的流订阅都会被取消。但是流本身不会被取消并且数据保留在内存中吗?

我怀疑:使用该应用程序时,当我调用PrepareList(...)多个位置(多次)时,它会在内存上加载越来越多的数据,并且永远不会清理它。一段时间后,该应用程序消耗了所有可用内存并通过给出上述错误而崩溃,这告诉我没有任何意义。

随着PrepareList(...)使用应用程序的执行次数越来越多,iPhone 7 会发热,这一点我很容易感觉到。我什至用 iPhone 12 进行了测试,它不像 iPhone 7 那样发热,但也会崩溃。

我什至尝试将 dispose 添加到这两个类中:

 @override
  void dispose() {
    super.dispose();
  }
Run Code Online (Sandbox Code Playgroud)

......但仍然没有帮助。

我的流实现是真的吗?我怀疑这可能是这次崩溃的根本问题吗?

附录2

我一直在努力。我以某种方式使用该应用程序,因此PrepareList("...");会触发多次。我也在开发工具中观察了内存使用情况。我可以观察到内存使用量随着时间的推移而增加。这次我遇到了一个新错误,说了一些更具体的内容:

[ServicesDaemonManager] interruptionHandler is called. -[FontServicesDaemonManager connection]_block_invoke
[tcp] tcp_input [C17.1.1:3] flags=[R] seq=3749683210, ack=0, win=0 state=LAST_ACK rcv_nxt=3749683210, snd_una=3584722489
[tcp] tcp_input [C17.1.1:3] flags=[R] seq=3749683210, ack=0, win=0 state=CLOSED rcv_nxt=3749683210, snd_una=3584722489
* thread #46, name = 'DartWorker', stop reason = EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=1450 MB, unused=0x0)
    frame #0: 0x0000000108ef4d0c Flutter`dart::CompilerPass_TypePropagation::DoBody(dart::CompilerPassState*) const + 1644
Flutter`dart::CompilerPass_TypePropagation::DoBody:
->  0x108ef4d0c <+1644>: str    xzr, [x20, x28, lsl #3]
    0x108ef4d10 <+1648>: ldr    x8, [x22, #0x48]
    0x108ef4d14 <+1652>: cmp    x24, x8
    0x108ef4d18 <+1656>: b.ge   0x108ef4d84               ; <+1764>
Target 0: (Runner) stopped.
Lost connection to device.
Run Code Online (Sandbox Code Playgroud)

这次,它说EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=1450 MB, unused=0x0)。看起来,内存使用量随着时间的推移而增加。为什么当用户离开手机屏幕时内存没有释放?(我通过怀疑即使用户移动到另一个屏幕后流仍然占用内存来问这个问题......)

edn*_*edn 1

我知道我的问题没有真正组织起来,我无法提供复制问题的正确代码。经过多次尝试,我似乎解决了这个问题。因为这种情况只是偶尔发生,所以图片对我来说仍然很模糊,但我至少知道是什么解决了我的问题。

提供完整的代码仍然很困难,但我将尝试在这里用一些伪代码解释什么解决了我的问题,我希望这对可能遇到类似问题的其他人有所帮助。

无论我对此多么不满意,应用程序的某些部分都会不可避免地存在一些嵌套流。在一页上,我有多个嵌套流构建器,但我将仅在下面说明 2 个流的情况:

StreamBuilder1
   --> Read data A from database
   StreamBuilder2
      ---> Read data B from database (by using data from data A)
Run Code Online (Sandbox Code Playgroud)

从数据库读取数据 B时,需要使用StreamBuilder1中的一些输入。存在依赖性。

这 2 个流偶尔会导致页面刷新。可能导致 EXC_BAD_ACCESSS 的原因之一似乎是试图读取已经释放的部分内存。我“怀疑”的是,当StreamBuilder2需要使用数据 A从数据库读取自己的数据时,数据 A 有时并不真正存在于内存中。

我做了以下更改...

StreamBuilder1
   --> Read data A from database
   FutureBuilder2
      ---> Read data B from database (by using data from data A)
Run Code Online (Sandbox Code Playgroud)

我用 FutureBuilder 替换了 StreamBuilder2,意外的崩溃消失了。

我本质上有多个嵌套流构建器。如果能为所有人提供流媒体那就太好了,但这并不是必须的。只需要使用流构建器来实现其中一个数据源,我就是这样做的。我用未来的构建器替换了所有其他流。从那时起,我就没有遇到过 EXC_BAD_ACCESS 崩溃了。

我可能会误解事情的运作方式或这种变化如何解决我的问题。但这至少是我所做的,并且最终真正解决了我的问题。如果有人有任何进一步的意见,我们非常欢迎他/她分享它们,因为我非常有兴趣了解使用嵌套流时需要考虑的事情,因为很难找到好的例子。

旁注:我什至研究了一点 BLoC,希望它可以帮助解决这个问题。我想对于已经知道如何使用它的人来说这很容易。但这是相当令人畏惧的,特别是如果你想在从 firebase 读取数据时将它用于多个流,并且我找不到任何如何使用 BLoC 执行此操作的示例。


附录(2022 年 6 月 24 日): 我仍然遇到这个偶尔发生的问题。Android 中尚未发生这种情况,但该应用程序因未知原因而关闭。在我上面提到的更改之后,这种情况不会经常发生,但问题还没有完全消失。