处理图像/视频和导航器的内存使用情况

Handle memory usage with images / videos and Navigators flutter

对不起大 post 提前...

我的应用程序是一个与 Instagram 非常相似的社交网络。

模式 :

  1. 用户个人资料(包含缩略图列表,未播放视频)
  2. Select照片(打开页面Navigator.push()有照片/播放视频
  3. Select 另一个用户个人资料(例如在视频的评论中)
  4. 再次查看缩略图列表,然后 post 带有照片的列表 - 播放视频等等...

这就像一个无限的“配置文件 - 提要”循环。有了这样的逻辑,在我 运行 进入 OutOfMemory 错误之前,我用 Navigator.push() 达到了或多或少的 30 页。

Flutter 工具只显示 lost connection to device,但我使用 Navigator 的次数越多,应用程序变得越慢,最终崩溃,所以我 99% 确定这是由于内存使用造成的。
由于 post 列表中的滚动,这种情况在 100% 的时间或多或少会发生 1 页差异。

如果我不在图片/视频列表中滚动太多,内存使用量每页或多或少会增加20MB。
我已经计划缩小我的图像,但这充其量只是延迟了问题。

问题 :

在这种情况和这种逻辑下,这意味着如果我可能在 Instagram 中浏览 100 页,这也会在 100% 时崩溃。

我不确定是否有人会走那么远,也许这就是他们所指望的...如果没有办法阻止 OutOfMemory,唯一的解决方案可能是将其延迟到用户例如至少看了 100 页...

WORKAROUND 我在理论上找到了但不确定在代码中是否可行 :

到目前为止我想到的唯一解决方案是允许用户 push() 一定数量的页面,并在 Navigator 堆栈中最多保留 20 页,但不删除第一个页。 所以内存中总会有20页。

如果这是唯一的解决方案,有人可以提供有关如何处理 Navigator 堆栈中特定页面的示例吗?

此外,我注意到当您返回 Navigator.pop() 时,内存并没有减少,为什么?

编辑 :

为了显示照片/视频,我基本上使用了 2 packages :

我还在应用程序的一部分中使用了 Slivers,但再次使用了前面提到的包中的 SliverStaggeredGrid.countBuilder

我认为您已经正确指出,根本问题在于推送大量页面的“无限”性质,同时将所有旧页面保留在内存中。

您尚未发布任何源代码,但我认为此时它不相关。而且从问题描述上看,你的能力很强,所以我们大概可以从高层次上讨论这个问题。正如一些评论指出的那样,也许每个页面都有优化的空间,但即便如此,它也只是延迟问题。简单来说,即使每个页面只是一个Text widget,它最终也会运行陷入OOM问题。

解决此问题的一种方法是构建您自己的路由堆栈。在最近的 Flutter 更新中,这变得容易多了,他们引入了所谓的“Navigator 2.0”,您可以从 official docs.

了解更多信息

本质上,您可以拥有自己的“堆栈”(数据结构)来跟踪用户单击的内容,以便“后退按钮”正常运行。但是在每次跳转时,您不是推送新页面,而是字面上“替换”当前页面。同样,当用户返回时,您不会“弹出”导航器,因为它下面什么也没有。相反,您查找自己的“历史堆栈”并重新加载上一页的内容。这样,您可以进一步延迟问题(直到您的堆栈溢出,就像本网站的名称一样)。

一旦完成,也许您可​​以做进一步的优化,就是让一些最近的页面保持活动状态,而不是立即销毁它们。例如,也许您可​​以让最近的 3-5 个页面保持活动状态,以便用户可以更快地返回到它们(无需加载)。无论如何,这个可选的优化可能会覆盖 95% 的用例。


编辑:

既然你在评论中提到你想要一个 Navigator 2.0 的例子,我拿出我的 IDE 并为你做了一个快速演示。

如您所见,用户可以打开几乎无限量的新页面,并且可以返回到所有这些页面;然而,在任何给定时间,内存中只有 3 个页面,其他页面将被完全销毁并在需要时重新创建。

我在源代码中写了一些简短的注释来帮助您,但是您始终可以从官方文档中学习 Navigator 2.0。

完整源代码:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // This list holds the real "stack": anything user has ever clicked.
  // But it only holds minimal info - just the page title.
  final List<String> _titles = [
    'Home Page',
  ];

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Navigator(
        // The `pages` property here is the most important thing in this demo.
        // It literally represents the stack of pages on the screen.
        // If we make each item in `_titles` into a page, it'll build them all.
        pages: _getPages()
            .map((title) => MaterialPage(
                  key: ValueKey(title),
                  child: _buildPage(title),
                ))
            .toList(),
        onPopPage: (route, result) {
          // When user pops a page, we remove it from our list as well.
          setState(() => _titles.removeLast());
          return route.didPop(result);
        },
      ),
    );
  }

  List<String> _getPages() {
    print("Current list: $_titles");
    // Uncomment the following line, to skip "optimization" altogether.
    // return _titles;

    // Optimization: only build the top-most 3 pages.
    // First, don't optimize if there are only 3 or fewer pages.
    if (_titles.length <= 3) return _titles;
    // Otherwise, we only return the last 3 items, so the navigator
    // will only build those 3 pages, omitting anything beneath them.
    final last3items = [
      _titles[_titles.length - 3],
      _titles[_titles.length - 2],
      _titles[_titles.length - 1],
    ];
    print("Optimizing, only build the top 3 pages: $last3items");
    return last3items;
  }

  Widget _buildPage(title) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: GridView.count(
        crossAxisCount: 4,
        children: [
          for (int i = 1; i <= 10; i++)
            Padding(
              padding: EdgeInsets.all(8),
              child: ElevatedButton(
                onPressed: () {
                  final time = DateTime.now().millisecondsSinceEpoch;
                  setState(() {
                    // When user clicks on a button, we add it to our list
                    _titles.add('Page $i: $time');
                  });
                },
                child: Text('Open Page $i'),
              ),
            ),
        ],
      ),
    );
  }
}