You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter应用滚动Feed时视频播放引发内存溢出崩溃的解决方法

解决Flutter Feed滚动时的OutOfMemory崩溃问题

问题概述

Flutter应用的Feed流包含视频和图片内容,滚动一段时间后应用出现崩溃,debug和release模式下均会触发,崩溃原因是OutOfMemoryError。

崩溃日志

W/com.app.( 3621): Throwing OutOfMemoryError "Failed to allocate a 65548 byte allocation with 371000 free bytes and 362KB until OOM, target footprint 201326592, growth limit 201326592" (VmSize 2037760 kB)
I/com.app.( 3621): Waiting for a blocking GC Alloc
I/com.app.( 3621): Clamp target GC heap from 215MB to 192MB

现有实现代码

Feed列表代码

return InViewNotifierList(
  controller: _scrollController,
  itemCount: homeController.feedPostList.length + (homeController.isLastPage.value ? 0 : 1),
  initialInViewIds: ['0'],
  isInViewPortCondition: (double deltaTop, double deltaBottom, double viewPortDimension) {
    return deltaTop < (0.4 * viewPortDimension) && deltaBottom > (0.3 * viewPortDimension);
  },
  builder: (context, index) {
    return Column(
      children: [
        homeController.feedPostList[index].feedType!.toLowerCase() == "image"
            ? FeedImageView(userFeedPost: homeController.feedPostList[index])
            : InViewNotifierWidget(
                id: '$index',
                builder: (context, isInView, child) {
                  return VideoWidget(
                    thumbnailUriSmall: homeController.feedPostList[index].thumbnailUriSmall!,
                    thumbnailUriLarge: homeController.feedPostList[index].thumbnailUriLarge!,
                    play: isInView,
                    url: homeController.feedPostList[index].contentUri!,
                  );
                },
              ),
      ],
    );
  },
);

VideoWidget代码

class VideoWidget extends StatefulWidget {
  final String url;
  final bool play;
  final String thumbnailUriLarge;
  final String thumbnailUriSmall;

  const VideoWidget({
    Key? key,
    required this.url,
    required this.play,
    required this.thumbnailUriLarge,
    required this.thumbnailUriSmall,
  }) : super(key: key);

  @override
  _VideoWidgetState createState() => _VideoWidgetState();
}

class _VideoWidgetState extends State<VideoWidget> {
  late VideoPlayerController _controller;
  ChewieController? _chewieController;

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

  @override
  void didUpdateWidget(VideoWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.play != widget.play) {
      if (_chewieController != null) {
        if (widget.play) {
          _chewieController!.play();
        } else {
          _chewieController!.pause();
        }
      }
    }
  }

  void _initVideoPlayer() async {
    try {
      _controller = VideoPlayerController.networkUrl(Uri.parse(widget.url));
      await _controller.initialize();
      _chewieController = ChewieController(
        videoPlayerController: _controller,
        autoInitialize: true,
        autoPlay: widget.play,
        looping: true,
        allowFullScreen: false,
        showControls: false,
        placeholder: const Center(
          child: CircularProgressIndicator(),
        ),
      );
      setState(() {});
      print("Controller initialized");
    } catch (e) {
      print("Error initializing video: $e");
    }
  }

  @override
  void dispose() {
    _chewieController?.dispose();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return _chewieController != null && _chewieController!.videoPlayerController.value.hasError
        ? const Center(
            child: Text("Error"),
          )
        : _chewieController != null && _chewieController!.videoPlayerController.value.isInitialized
            ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: Chewie(
                  controller: _chewieController!,
                ),
              )
            : Center(
                child: CachedNetworkImage(
                  imageUrl: widget.thumbnailUriLarge,
                  errorWidget: (context, error, stackTrace) => CachedNetworkImage(
                    imageUrl: widget.thumbnailUriSmall,
                    errorWidget: (context, error, stackTrace) => const Center(
                      child: Text("Temporary video is not available"),
                    ),
                  ),
                ),
              );
  }
}

优化方案

1. 延迟初始化视频控制器,离开可视区域时销毁

当前每个VideoWidget创建时就初始化控制器,即使不在屏幕内,导致内存中存在大量闲置的视频控制器。修改逻辑:仅当视频进入可视区域(play为true)时才初始化控制器,离开时销毁控制器释放内存。

修改后的VideoWidget核心代码:

class _VideoWidgetState extends State<VideoWidget> {
  VideoPlayerController? _controller;
  ChewieController? _chewieController;

  @override
  void initState() {
    super.initState();
    if (widget.play) _initVideoPlayer();
  }

  @override
  void didUpdateWidget(VideoWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.play != widget.play) {
      if (widget.play) {
        _controller == null ? _initVideoPlayer() : _chewieController?.play();
      } else {
        _chewieController?.pause();
        _disposeControllers(); // 离开时销毁控制器
      }
    }
  }

  void _disposeControllers() {
    _chewieController?.dispose();
    _controller?.dispose();
    _chewieController = null;
    _controller = null;
    setState(() {});
  }

  // 其余方法保持不变
}

2. 缩小视频触发播放的可视区域范围

当前的可视区域条件会导致多个视频同时进入触发范围,同时初始化和播放,占用大量内存。调整条件,仅让视频进入屏幕核心区域时才触发:

isInViewPortCondition: (double deltaTop, double deltaBottom, double viewPortDimension) {
  // 仅当视频覆盖屏幕中间60%区域时触发
  return deltaTop < (0.7 * viewPortDimension) && deltaBottom > (0.3 * viewPortDimension);
},

3. 确保列表Item被正确回收

给每个Feed Item添加唯一Key,帮助Flutter框架正确识别和回收闲置Widget:

builder: (context, index) {
  return Column(
    key: Key('feed-item-$index'), // 添加唯一Key
    children: [
      // 原有代码
    ],
  );
},

4. 优化图片内存占用

对于Feed中的图片,使用CachedNetworkImage时设置缓存尺寸限制,避免加载过大的图片到内存:

CachedNetworkImage(
  imageUrl: widget.thumbnailUriLarge,
  maxWidthDiskCache: 400, // 根据实际需求设置
  maxHeightDiskCache: 400,
  fit: BoxFit.cover, // 适配容器,避免不必要的内存占用
  // 其余配置保持不变
)

5. 监听App生命周期,后台时释放资源

当App退到后台时,暂停所有视频并销毁控制器,回到前台时再按需初始化:

// 在Feed页面的State中添加
@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addObserver(this);
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  super.didChangeAppLifecycleState(state);
  if (state == AppLifecycleState.paused) {
    // 遍历所有视频Widget,暂停并销毁控制器
    // 或者通过状态管理通知所有VideoWidget释放资源
  } else if (state == AppLifecycleState.resumed) {
    // 按需重新初始化可视区域内的视频
  }
}

@override
void dispose() {
  WidgetsBinding.instance.removeObserver(this);
  super.dispose();
}

内容的提问来源于stack exchange,提问作者Harsh Navapara

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 01:22:03