为什么我的 Flutter 应用程序随机崩溃以及如何查找潜在的内存泄漏 - EXC_RESOURCE RESOURCE_TYPE_MEMORY

Mar*_*oso 10 android ios flutter google-cloud-firestore

我的 flutter 应用程序在所有平台上运行良好几个月,但随机开始崩溃,我无法找到问题或弄清楚为什么它会突然发生。崩溃会关闭应用程序,并且应用程序无法再次打开。

\n

我设置了 Crashlytics 和 Sentry 来检查崩溃日志,但都没有显示问题所在。我只能通过使用连接到 VSCode 的真实设备重现崩溃来解决以下错误。我提供了两个,错误几乎总是发生在特定屏幕上或该屏幕之前的屏幕上。该错误发生在某些 Android 设备上,但在我亲自测试过的 Samsung Galaxy S21 或 S8 上从未出现过。在 iPhone 6 上,应用程序在我到达有问题的屏幕之前就崩溃了。该错误不会发生在模拟器上。一旦它在 iPhone 12 上崩溃并停止,如果我尝试打开该应用程序,它甚至不会在手机上启动。

\n

我尝试使用 CachedNetworkImage 而不是仅使用 NetworkImage 来更新 Flutter 和 XCode,并且确保if(mounted)在任何调用之前进行调用,以最大程度地减少内存泄漏的可能性,并且通过重写该方法来setState正确处置任何内存泄漏。我什至不知道从哪里开始寻找这个问题。StreamSubscriptionsdispose()

\n

请帮我确定这里发生了什么。我可以使用什么方法来查找导致此崩溃的原因?

\n

在 iPhone 12 上运行:

\n
Error 1:\n* thread #15, name = 'io.worker.3', stop reason = EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=2098 MB, unused=0x0)\n    frame #0: 0x000000010f011538 Flutter`ycc_rgb_convert + 140\nFlutter`ycc_rgb_convert:\n->  0x10f011538 <+140>: strb   w21, [x5]\n    0x10f01153c <+144>: ldr    x21, [x12, x19, lsl #3]\n    0x10f011540 <+148>: ldr    x20, [x11, x20, lsl #3]\n    0x10f011544 <+152>: add    x20, x20, x21\nTarget 0: (Runner) stopped.\nLost connection to device.\n\nError 2: \n[Process] 0x11f000d30 - NetworkProcessProxy::didClose (Network Process 0 crash)\n[ServicesDaemonManager] interruptionHandler is called. -[FontServicesDaemonManager connection]_block_invoke\n* thread #15, name = 'io.worker.3', stop reason = EXC_RESOURCE RESOURCE_TYPE_MEMORY (limit=2098 MB, unused=0x0)\n    frame #0: 0x000000011336f7ac Flutter`ycc_rgb_convert + 140\nFlutter`ycc_rgb_convert:\n->  0x11336f7ac <+140>: strb   w21, [x5]\n    0x11336f7b0 <+144>: ldr    x21, [x12, x19, lsl #3]\n    0x11336f7b4 <+148>: ldr    x20, [x11, x20, lsl #3]\n    0x11336f7b8 <+152>: add    x20, x20, x21\nTarget 0: (Runner) stopped.\nLost connection to device.\n\nFlutter Version:\nFlutter 3.7.0 \xe2\x80\xa2 channel stable \xe2\x80\xa2 https://github.com/flutter/flutter.git\nFramework \xe2\x80\xa2 revision b06b8b2710 (2 days ago) \xe2\x80\xa2 2023-01-23 16:55:55 -0800\nEngine \xe2\x80\xa2 revision b24591ed32\nTools \xe2\x80\xa2 Dart 2.19.0 \xe2\x80\xa2 DevTools 2.20.1\n\nRunning flutter doctor...\nDoctor summary (to see all details, run flutter doctor -v):\n[\xe2\x9c\x93] Flutter (Channel stable, 3.7.0, on macOS 12.6.3 21G419 darwin-x64, locale en-GB)\n[\xe2\x9c\x93] Android toolchain - develop for Android devices (Android SDK version 30.0.3)\n[\xe2\x9c\x93] Xcode - develop for iOS and macOS (Xcode 14.2)\n[\xe2\x9c\x93] Chrome - develop for the web\n[\xe2\x9c\x93] Android Studio (version 2021.2)\n[\xe2\x9c\x93] VS Code (version 1.74.3)\n[\xe2\x9c\x93] Connected device (3 available)\n[\xe2\x9c\x93] HTTP Host Availability\n\n\xe2\x80\xa2 No issues found!\n
Run Code Online (Sandbox Code Playgroud)\n

编辑:

\n

感谢您的回复。崩溃主要发生在应用程序的一个特定部分,即我在一个屏幕(iPhone 6)中加载图像,或者在加载图像屏幕后(iPhone12)加载一两个屏幕。我升级到 Flutter 3.7,试图解决仍然存在的问题。

\n

我认为这是一个图像问题,因为 ChatGPT 说:“函数名称“ycc_rgb_convert”表明它可能与图像处理有关,因为 YCC (YUV) 是图像和视频压缩中使用的颜色空间,但它并没有证实这一点它与加载图像有关。” 当然,这只是一个线索,并不能确定。

\n

它也不会发生在所有设备上。它发生在 iPhone 6 上,在 iPhone 12 上发生需要更长的时间,而在三星 Galaxy S21 或 S8 上根本不会发生。该错误大约 2 周前开始发生,我只是在尝试解决此问题时才开始使用 CacheNetworkImage,之前我不需要它,而且说实话,现在它并没有真正的帮助。

\n

崩溃发生在地点屏幕(或在该屏幕进入堆栈后不久),我在其中加载了“地点项目”的条子网格。位置项是一个网格图块,它具有主图像和头像图像,以及放置在网格图块页眉和页脚中的 like_button 包中的一些文本和 2 个图标按钮。单击某个地点项目会将您带到地点详细信息屏幕,iPhone12 有时会在该屏幕上发生崩溃。如果我完全避开 iPhone 6 上的地点屏幕,就不会发生崩溃。但如果我在地点屏幕中加载区区 4 个地点项目,iPhone 6 上就会发生崩溃。

\n

我首先加载 6 个位置项,然后在用户滚动时加载另外 4 个位置项来进行分页。我加载的主图像最大,不超过 1MB(平均大小可能为 400-500kb),头像图像更小,最大可能 200kb,最小不到 20kb。这些图像与它们一直以来属于地方文档的图像相同。

\n

地点屏幕有一个从 200kb 的资源加载的背景图像,并且也是预先缓存的(我为解决此问题所做的另一件事,但以前没有或没有必要)。我还对正在加载的每个缓存网络图像使用相同的占位符图像,这也是 200kb 并且是预先缓存的。

\n

您能否指导我如何识别内存泄漏?尝试在 DevTools 中使用内存分析器不起作用,因为崩溃发生时它会自动断开连接,除了上面给出的错误之外,我从控制台没有得到任何信息(仅适用于 iPhone 12,iPhone 6 根本没有日志)。当我在 Galaxy s8 上以配置文件模式运行它时,保留的大小从未超过 25MB,而我采取了可能导致 iPhone 6 和 12 中崩溃的所有步骤,并且它运行得像黄油一样流畅。没有内存峰值。

\n

当我在配置文件模式下运行 iPhone 6 时,在断开连接之前,dart 堆保持在 16MB 左右,从启动开发工具到崩溃,保留大小保持在 20-30MB 左右,没有明显的内存峰值,但也许我正在使用该工具错误地?我不知道...

\n

Sentry.io 记录了这些 iPhone 6 崩溃并表示:“内存不足:操作系统很可能终止了您的应用程序,因为它过度使用了 RAM。” 但它没有提供进一步的细节。

\n

Sentry 也是尝试诊断此问题的新成员。

\n

这是地点项目代码:

\n
import 'package:flutter/material.dart';\nimport 'package:font_awesome_flutter/font_awesome_flutter.dart';\nimport 'package:like_button/like_button.dart';\nimport 'package:provider/provider.dart';\nimport 'package:firebase_auth/firebase_auth.dart';\nimport 'package:cached_network_image/cached_network_image.dart';\n\nimport '../data/color_palette.dart';\nimport '../widgets/pricing_icons.dart';\nimport '../models/place.dart';\nimport '../screens/place_detail_screen.dart';\nimport '../models/user.dart';\nimport '../services/database.dart';\nimport './custom_dialogbox.dart';\n\nclass PlaceItem extends StatelessWidget {\n  final Place venue;\n  PlaceItem(this.venue);\n\n  final FirebaseAuth _auth = FirebaseAuth.instance;\n  final DatabaseService _db = DatabaseService();\n\n  @override\n  Widget build(BuildContext context) {\n    Size size = MediaQuery.of(context).size;\n    final venue = Provider.of<Place>(context, listen: false);\n    return ClipRRect(\n      clipBehavior: Clip.antiAlias,\n      borderRadius: BorderRadius.circular(20),\n      child: GestureDetector(\n        onTap: () {\n          ScaffoldMessenger.of(context).hideCurrentSnackBar();\n          Navigator.of(context).pushNamed(\n            PlaceDetailScreen.routeName,\n            arguments: venue.venueId,\n            // Extract in the screen we'll nav to with ModalRoute\n          );\n        },\n        child: GridTile(\n          header: Consumer<Place>(\n              builder: (ctx, venue, child) => _gridTileHeader(context)),\n          footer: _gridTileFooter(size),\n          // Venue Image\n          child: CachedNetworkImage(\n            imageUrl: venue.venueImageUrl,\n          \n            fadeInDuration: const Duration(milliseconds: 500),\n\n            placeholder: (context, url) => Image.asset(\n              'assets/images/tile_background.png',\n              fit: BoxFit.cover,\n            ),\n            fit: BoxFit.cover,\n            errorWidget: (context, url, error) => Container(\n              decoration: const BoxDecoration(\n                image: DecorationImage(\n                  image: AssetImage(\n                    'assets/images/tile_background.png',\n                  ),\n                  fit: BoxFit.cover,\n                ),\n              ),\n              child: const Center(\n                child: Icon(Icons.error, color: Colors.grey,),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _gridTileHeader(context) {\n    final user = _auth.currentUser;\n    final animationDuration = const Duration(milliseconds: 700);\n    return StreamBuilder<UserData>(\n        stream: DatabaseService(uid: user.uid).userData,\n        builder: (context, snapshot) {\n          if (snapshot.hasData) {\n            UserData userData = snapshot.data;\n            bool isBookmarked = userData.bookmarks.contains(venue.venueId);\n            bool isFaved = userData.favorites.contains(venue.venueId);\n\n            return GridTileBar(\n              backgroundColor: Colors.black.withOpacity(0.4),\n              // Animated bookmark button\n              leading: LikeButton(\n                // key: _globalKey,\n                isLiked: isBookmarked,\n                circleColor: const CircleColor(\n                  start: firstColor,\n                  end: sixthColor,\n                ),\n                bubblesColor: const BubblesColor(\n                  dotPrimaryColor: sixthColor,\n                  dotSecondaryColor: Colors.red,\n                ),\n                // size: 100, // used when testing for closer look\n                // size: 50, // size of containing circle\n                likeBuilder: (bool bookmarked) {\n                  return Icon(\n                    Icons.bookmark,\n                    color: isBookmarked ? sixthColor : Colors.white,\n                    // size: 75, // used when testing for closer look\n                    size: 20,\n                  );\n                },\n                animationDuration: animationDuration,\n\n                onTap: (bookmarked) async {\n                  print(bookmarked);\n                  print(isBookmarked);\n                  // return !bookmarked;\n                  try {\n                    // // if user's email isn't verified\n                    if (!user.emailVerified) {\n                      showDialog(\n                          context: context, builder: (ctx) => VerifyEmail());\n                      return bookmarked; // do nothing\n                    } else {\n                      // actual functionality\n                      DatabaseService(uid: user.uid)\n                          .toggleToUserList('bookmarks', venue.venueId);\n                      ScaffoldMessenger.of(context).hideCurrentSnackBar();\n                      ScaffoldMessenger.of(context).showSnackBar(\n                        SnackBar(\n                          elevation: 10,\n                          backgroundColor: firstColor,\n                          content: !isBookmarked\n                              ? const Text('Venue Added to Bookmarks!')\n                              : const Text('Venue Removed from Bookmarks'),\n                          duration: const Duration(milliseconds: 2500),\n                          action: SnackBarAction(\n                            textColor: sixthColor,\n                            label: 'UNDO',\n                            onPressed: () async {\n                              // widget.venue.toggleBookmarkStatus();\n                              DatabaseService(uid: user.uid)\n                                  .toggleToUserList('bookmarks', venue.venueId);\n\n                              // reverse it\n                              _db.updateLikesBookmarks(\n                                venue.venueId,\n                                venue.vendorId,\n                                venue.vendorName,\n                                venue.address['city'],\n                                'bookmarks',\n                                bookmarked,\n                              );\n                            },\n                          ),\n                        ),\n                      );\n\n                      // Track bookmarks\n                      // putting "await" with the function stops the animation\n                      _db.updateLikesBookmarks(\n                        venue.venueId,\n                        venue.vendorId,\n                        venue.vendorName,\n                        venue.address['city'],\n                        'bookmarks',\n                        !bookmarked,\n                      );\n\n                      return !bookmarked;\n                    }\n                  } catch (e) {\n                    print(e.toString());\n                    return bookmarked; // dont change it if there was an error\n                  }\n                },\n              ),\n              title: venue.ownerManaged\n                  ? const Icon(\n                      Icons.verified,\n                      color: Colors.white,\n                    )\n                  : const Text(''),\n\n              // Animated Favorites Button\n              trailing: LikeButton(\n                // key: _globalKey,\n                isLiked: isFaved,\n                circleColor: const CircleColor(\n                  start: firstColor,\n                  end: Colors.red,\n                ),\n                bubblesColor: const BubblesColor(\n                  dotPrimaryColor: secondColor,\n                  dotSecondaryColor: Colors.red,\n                ),\n                // size: 100, // used when testing for closer look\n                // size: 50, // sizze of containing circle\n                likeBuilder: (bool faved) {\n                  return Icon(\n                    Icons.favorite,\n                    color: isFaved ? secondColor : Colors.white,\n                    // size: 75, // used when testing for closer look\n                    size: 20,\n                  );\n                },\n                animationDuration: animationDuration,\n\n                onTap: (faved) async {\n                  // print(faved);\n                  // print(isFaved);\n                  // return !faved;\n                  try {\n                    // // if user's email isn't verified\n                    if (!user.emailVerified) {\n                      showDialog(\n                          context: context, builder: (ctx) => VerifyEmail());\n                      return faved; // do nothing\n                    } else {\n                      // actual functionality\n                      DatabaseService(uid: user.uid)\n                          .toggleToUserList('favorites', venue.venueId);\n                      ScaffoldMessenger.of(context).hideCurrentSnackBar();\n                      ScaffoldMessenger.of(context).showSnackBar(\n                        SnackBar(\n                          // behavior: SnackBarBehavior.floating,\n                          elevation: 10,\n                          backgroundColor: firstColor,\n                          content: !isFaved\n                              ? const Text('Venue Added to Favorites!')\n                              : const Text('Venue Removed from Favorites'),\n                          duration: const Duration(milliseconds: 2500),\n                          action: SnackBarAction(\n                            textColor: sixthColor,\n                            label: 'UNDO',\n                            onPressed: () async {\n                              // widget.venue.toggleBookmarkStatus();\n                              DatabaseService(uid: user.uid)\n                                  .toggleToUserList('favorites', venue.venueId);\n\n                              // reverse it\n                              _db.updateLikesBookmarks(\n                                venue.venueId,\n                                venue.vendorId,\n                                venue.vendorName,\n                                venue.address['city'],\n                                'likes',\n                                faved,\n                              );\n                            },\n                          ),\n                        ),\n                      );\n\n                      // Track bookmarks\n                      // putting "await" with the function stops the animation\n                      _db.updateLikesBookmarks(\n                        venue.venueId,\n                        venue.vendorId,\n                        venue.vendorName,\n                        venue.address['city'],\n                        'likes',\n                        !faved,\n                      );\n\n                      return !faved;\n                    }\n                  } catch (e) {\n                    print(e.toString());\n                    return faved; // dont change it if there was an error\n                  }\n                },\n              ),\n            );\n          } else if (user.isAnonymous) {\n            return GridTileBar(\n              backgroundColor: Colors.black.withOpacity(0.35),\n              leading: IconButton(\n                icon: const Icon(\n                  Icons.bookmark_border,\n                  color: Colors.white,\n                  size: 20,\n                ),\n                onPressed: () {\n                  showDialog(context: context, builder: (ctx) => AnonDialog());\n                },\n              ),\n              title: const Text(''),\n              trailing: IconButton(\n                icon: const Icon(\n                  Icons.favorite_border,\n                  size: 20,\n                ),\n                onPressed: () {\n                  showDialog(context: context, builder: (ctx) => AnonDialog());\n                },\n              ),\n            );\n          } else {\n            // fake tile bar with no functionality\n            return GridTileBar(\n              backgroundColor: Colors.black.withOpacity(0.35),\n              leading: IconButton(\n                icon: const Icon(\n                  Icons.bookmark_border,\n                  color: Colors.white,\n                  size: 20,\n                ),\n                onPressed: () {},\n              ),\n              title: const Text(''),\n              trailing: IconButton(\n                icon: const Icon(\n                  Icons.favorite_border,\n                  size: 20,\n                ),\n                onPressed: () {},\n              ),\n            );\n          }\n        });\n  }\n\n  Widget _gridTileFooter(size) {\n    return GridTileBar(\n      backgroundColor: Colors.black87,\n      // title\n      title: Padding(\n        padding: const EdgeInsets.only(top: 1.0, bottom: 3),\n        child: Text(\n          venue.venueName,\n          overflow: TextOverflow.ellipsis,\n          softWrap: true,\n          maxLines: 2,\n          textAlign: TextAlign.left,\n          style: const TextStyle(\n              color: Colors.white,\n              fontFamily: 'Poppins',\n              fontSize: 12,\n              height: 1),\n        ),\n      ),\n      // subtitle column\n      subtitle: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: <Widget>[\n          // price icons\n          FittedBox(\n            fit: BoxFit.fitWidth,\n            child: PricingIconsDB(venue),\n          ),\n          // Google rating\n          Container(\n            padding: const EdgeInsets.only(\n              left: 2,\n              top: 2,\n            ),\n            width: size.width * 0.25,\n            child: FittedBox(\n              fit: BoxFit.fitWidth,\n              child: Row(\n                children: [\n                  const Icon(\n                    FontAwesomeIcons.google,\n                    color: Colors.white70,\n                    // color: firstColor,\n                    size: 10,\n                  ),\n                  const Text(' Rating: '),\n                  Text(\n                    venue.googleRating == null\n                        ? '-'\n                        : venue.googleRating\n                            .toStringAsFixed(1)\n                            .replaceAll(RegExp(r"([.]*0)(?!.*\\d)"), ""),\n                    // '4.1',\n                    style: const TextStyle(color: firstColor),\n                  ),\n                  const Icon(\n                    Icons.star,\n                    // color: Colors.white,\n                    color: firstColor,\n                    size: 15,\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ],\n      ),\n      trailing: Container(\n          height: size.width * 0.126,\n          width: size.width * 0.126,\n          clipBehavior: Clip.antiAlias,\n          decoration: const BoxDecoration(\n            shape: BoxShape.circle,\n          ),\n          child: venue.vendorLogoUrl == null || venue.vendorLogoUrl.isEmpty\n              ? Container(\n                  color: Colors.black26,\n                )\n// What I used as placeholders before the crashes started:\n              // The gifs may be expensive in terms of RAM - 200KB - 500KB each\n              // ? Image.asset(\n              //     // restaurant or cafe\n              //     // loadedVenue\n              //     //         .businessCategories\n              //     //         .contains('c2')\n              //     venue.venueCategories["restaurant"]\n              //         ? 'assets/images/food_logo.gif'\n              //        

7ma*_*ada 3

正如您所期望的,崩溃与图像有关。\n当我构建一个使用大量图像的应用程序时,我遇到了与您相同的问题。

\n

当内存不足(大量实时图像)和/或同时运行许多 https 请求(下载图像)时,可能会发生崩溃。

\n

您可以清除图像缓存,然后在包含图像的网格上快速滚动以重现此问题。

\n

解决方案可能是:

\n

使用较低的图像分辨率,或者在显示图像之前调整图像大小。

\n

或者,如果您想使用高分辨率图像,您可以尝试通过调用您正在使用的图像evict上的方法来手动处理图像ImageProvider,如下例所示。

\n
    final imageProvider = NetworkImage(bytes);\n    imageProvider.evict();\n
Run Code Online (Sandbox Code Playgroud)\n

不过,调用前需要确保图片已完全加载evict,否则不会有任何效果。

\n

确保不要同时下载太多图像,这可能会很棘手。一种可能是手动下载 a 中的图像StatefulWidget,然后在 dispose 方法中取消下载。\xc2\xa0

\n

我已经开发了一个包来解决这个问题disposable_cached_images。尝试一下,如果它解决了问题,您可以使用它或修改源代码以获得所需的行为。

\n