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

Flutter应用ExoPlayer触发OutOfMemoryError崩溃问题求助

问题描述

应用运行一段时间后出现内存溢出崩溃,崩溃日志如下:

E/AndroidRuntime(15295): FATAL EXCEPTION: ExoPlayer:Loader:ProgressiveMediaPeriod
E/AndroidRuntime(15295): Process: com.app.dubfeed, PID: 15295
E/AndroidRuntime(15295): java.lang.OutOfMemoryError: Failed to allocate a 3944 byte allocation with 104656 free bytes and 102KB until OOM, target footprint 268435456, growth limit 268435456; giving up on allocation because <1% of heap free after GC.
E/AndroidRuntime(15295):        at java.util.Arrays.copyOf(Arrays.java:3578)
E/AndroidRuntime(15295):        at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:177)
E/AndroidRuntime(15295):        at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:638)
E/AndroidRuntime(15295):        at java.lang.StringBuilder.append(StringBuilder.java:226)
E/AndroidRuntime(15295):        at java.lang.String.replace(String.java:2843)
E/AndroidRuntime(15295):        at com.google.android.exoplayer2.util.Log.appendThrowableString(Log.java:295)
E/AndroidRuntime(15295):        at com.google.android.exoplayer2.util.Log.e(Log.java:254)
E/AndroidRuntime(15295):        at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:440)
E/AndroidRuntime(15295):        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
E/AndroidRuntime(15295):        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
E/AndroidRuntime(15295):        at java.lang.Thread.run(Thread.java:1012)

Manifest已配置android:hardwareAccelerated="false"和android:largeHeap="true",但崩溃仍未解决:

<application
        android:label=""
        android:name="${applicationName}"
        android:hardwareAccelerated="false"
        android:icon="@mipmap/ic_launcher"
        android:enableOnBackInvokedCallback="false"
        android:largeHeap="true"
        android:usesCleartextTraffic="true"
        android:roundIcon="@mipmap/ic_launcher_round">

Feed流视频播放器代码(由InviewNotifier控制,play为true时播放,false时暂停):

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();
          _clearCacheIfNeeded();
        }
      }
    }
  }

  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();
    _clearCacheIfNeeded();
    super.dispose();
  }

  void _clearCacheIfNeeded() {
    if (!widget.play) {
      final cacheManager = DefaultCacheManager();
      cacheManager.emptyCache().then((value) {
        print("Cache cleared");
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return _chewieController != null && _chewieController!.videoPlayerController.value.hasError
        ? 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"),
                ),
              ),
            ),
          )
        : _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"),
                    ),
                  ),
                ),
              );
  }
}
解决方案
  • 修复播放器资源泄漏:当前视频暂停时仅调用pause(),未释放控制器资源,Feed流列表频繁滚动会导致大量控制器实例堆积。修改didUpdateWidget逻辑,暂停时直接释放资源,播放时重新初始化:

    @override
    void didUpdateWidget(VideoWidget oldWidget) {
      super.didUpdateWidget(oldWidget);
      if (oldWidget.play != widget.play) {
        if (widget.play) {
          // 播放时重新初始化控制器
          _initVideoPlayer();
        } else {
          // 暂停时彻底释放资源
          _chewieController?.dispose();
          _controller.dispose();
          _chewieController = null;
          _clearCacheIfNeeded();
        }
      }
    }
    
  • 优化缓存清理策略:emptyCache()会清空全部缓存,过于激进且影响性能。改为只清理当前视频的缓存,同时给缓存设置大小上限:

    // 自定义缓存管理器,限制缓存大小
    class CustomCacheManager extends CacheManager {
      static const key = "videoCache";
      static CustomCacheManager? _instance;
    
      factory CustomCacheManager() {
        _instance ??= CustomCacheManager._();
        return _instance!;
      }
    
      CustomCacheManager._()
          : super(Config(
              key,
              stalePeriod: const Duration(days: 7),
              maxNrOfCacheObjects: 30, // 限制缓存文件数量
              maxCacheSize: 80 * 1024 * 1024, // 80MB缓存上限
            ));
    }
    
    // 修改清理逻辑
    void _clearCacheIfNeeded() {
      if (!widget.play) {
        final cacheManager = CustomCacheManager();
        cacheManager.removeFile(Uri.parse(widget.url)).then((_) {
          print("Current video cache cleared");
        });
      }
    }
    
  • 开启硬件加速:关闭硬件加速会让CPU承担视频解码,增加内存占用。将Manifest中的android:hardwareAccelerated改为true,让GPU处理解码任务:

    android:hardwareAccelerated="true"
    
  • 限制同时播放的视频数量:添加逻辑确保同一时刻只有一个视频处于播放状态,其他视频直接暂停并释放资源,避免多视频同时占用内存。

  • 用内存分析工具定位泄漏:使用Android Studio Profiler生成内存快照,检查是否存在未释放的控制器实例或其他内存泄漏点,针对性修复。

内容的提问来源于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:47:02