Flutter video_player报控制器dispose后仍被使用错误排查
Flutter video_player 连续播放多视频报used after disposed错误修复
问题现象
- 基于Flutter开发顺序连播的视频应用,实现逻辑为单条视频播放完成后销毁当前
VideoPlayerController,再初始化下一条视频的控制器 - 连续播放4-5条视频时运行正常,播放更多视频后抛出如下异常:
throw FlutterError( 'A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.', );
- 当前使用依赖版本:
video_player: ^2.4.2
附原问题触发代码:
void playVideo() async { if (DateTime.now().isAfter(videoData[index].start_from) && DateTime.now().isBefore(videoData[index].end_on)) { bool played_count = await CountQuery().whenComplete(() { }); file = File(videoData[index].file_link); if (file.existsSync() && !played_count) { isEnd = true; controller = VideoPlayerController.file(file) ..initialize().then((value) { setState(() { controller.play(); }); controller.addListener(() { // checking the duration and position every time // Video Completed// if (controller.value.duration == controller.value.position && isEnd) { setState(() { isEnd = false; }); CreateLog("video"); controller.dispose(); PlayNext(); } }); }) ..setLooping(false); } else { PlayNext(); } } else { PlayNext(); } } void PlayNext() { setState(() { index++; if (videoData.length > index) { // this.asset = videoData[index].file_link; this.file = File(videoData[index].file_link); playVideo(); } else { index = 0; // this.asset = videoData[index].file_link; this.file = File(videoData[index].file_link); playVideo(); } }); }
异常根因
- 监听器泄漏:每次创建新控制器添加的listener从未被移除,旧控制器被销毁后,残留的监听回调仍可能被系统触发,尝试访问已销毁的控制器实例直接抛错。
- 异步时序冲突:
initialize()是异步耗时操作,视频播放完成触发切源逻辑时,旧控制器的初始化回调、监听回调可能还在事件队列中等待执行,回调执行时控制器已经被dispose,或者全局controller变量已经被替换为新实例,访问时触发异常。 - 重复触发切源:视频播放到末尾时,position等于duration的状态会连续多次触发listener回调,原逻辑的
isEnd状态锁没有完全拦住重复执行,会出现同一个控制器被多次dispose、刚初始化的新控制器被误销毁的情况。 - 缺少生命周期校验:所有异步操作返回后没有校验当前Widget是否已挂载、控制器是否已销毁,直接执行操作很容易访问到已释放的对象。
修复方案
核心修复点:
- 每次销毁控制器前,先移除之前绑定的所有监听回调,避免残留回调执行
- 加切源全局锁,视频播放到末尾后立即上锁,避免重复触发dispose和切源逻辑
- 所有异步操作(初始化、数据库查询)返回后,二次校验组件挂载状态、切源锁状态,避免执行过期逻辑
- 不要在级联操作里直接给全局
controller赋值,等初始化完成、校验通过后再赋值,避免半初始化的控制器被其他逻辑访问 - 组件销毁时统一释放控制器资源,避免内存泄漏
修复后可运行的核心代码:
// 存储控制器监听回调,用于后续移除 late VoidCallback _videoListener; // 切源操作锁,避免重复触发切源逻辑 bool _isSwitching = false; // 全局播放器控制器 VideoPlayerController? controller; void playVideo() async { // 组件已销毁直接终止流程 if (!mounted) return; // 校验当前视频的有效播放时间窗口 final currentVideo = videoData[index]; final now = DateTime.now(); if (now.isBefore(currentVideo.start_from) || now.isAfter(currentVideo.end_on)) { PlayNext(); return; } final playedCount = await CountQuery(); // 异步返回后二次校验组件状态、切源状态 if (!mounted || _isSwitching) return; final videoFile = File(currentVideo.file_link); if (!videoFile.existsSync() || playedCount) { PlayNext(); return; } // 先释放上一个控制器的资源 _isSwitching = true; if (controller != null) { controller!.removeListener(_videoListener); await controller!.dispose(); controller = null; } // 创建新控制器,先不要赋值给全局变量 final newController = VideoPlayerController.file(videoFile) ..setLooping(false); // 定义监听逻辑 _videoListener = () { // 切源中、控制器未初始化完成直接返回 if (_isSwitching || !newController.value.isInitialized) return; // 用插件内置的isCompleted判断播放完成,避免手动对比的边界问题 if (newController.value.isCompleted) { _isSwitching = true; CreateLog("video"); PlayNext(); } }; newController.addListener(_videoListener); // 初始化新控制器 await newController.initialize(); // 初始化完成后再次校验状态,避免初始化过程中已经触发切源 if (!mounted || _isSwitching) { newController.removeListener(_videoListener); await newController.dispose(); return; } // 状态校验通过,再把新控制器赋值给全局变量 controller = newController; _isSwitching = false; setState(() { controller!.play(); }); } void PlayNext() { if (!mounted) return; setState(() { index++; // 播完列表回到第一个 if (index >= videoData.length) { index = 0; } playVideo(); }); } @override void dispose() { _isSwitching = true; if (controller != null) { controller!.removeListener(_videoListener); controller!.dispose(); } super.dispose(); }
优化建议
- 可以提前初始化下一个要播放的视频控制器,减少切视频时的加载黑屏时长,注意不要同时缓存太多控制器占用内存
- 切源过程中可以加载过渡loading组件,避免用户操作打断切源流程
- 可以给视频播放加错误监听,出现播放异常时自动跳过到下一个视频,提升稳定性
内容的提问来源于stack exchange,提问作者Aakash Ghimire
相关产品推荐
相关产品推荐

