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
相关产品推荐
相关产品推荐

