You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Flutter短视频Reels项目中基于BetterPlayer的预加载与OOM内存溢出问题排查求助

Flutter短视频Reels项目中基于BetterPlayer的预加载与OOM内存溢出问题排查求助

问题背景

我正在开发一款类似Instagram Reels或TikTok的Flutter短视频应用,用better_player_plus处理视频播放。为了提升滚动播放的流畅度,我做了预加载下5个、上2个视频的逻辑,但现在遇到了严重问题:滚动视频时APP直接崩溃,抛出OutOfMemoryError:

Throwing OutOfMemoryError "Failed to allocate a 40 byte allocation with 786656 free bytes and 768KB until OOM, target footprint 603979776, growth limit 603979776; giving up on allocation because <1% of heap free after GC." (VmSize 26710928 kB)

而且切换到下一个视频时还会有轻微的加载延迟。

已尝试的方案

我参考AI建议实现了LRU(最近最少使用)缓存类,用来管理BetterPlayer控制器,希望通过销毁闲置控制器来控制内存,但问题依然存在,具体实现如下:

1. 实现LRU缓存管理类

这个类的核心是达到容量上限时,自动销毁最久未使用的BetterPlayer控制器:

import 'dart:collection';
import 'package:better_player_plus/better_player_plus.dart';

class LRUCache<K, V> {
  final int capacity;
  final LinkedHashMap<K, V> _cache;

  LRUCache(this.capacity) : _cache = LinkedHashMap();

  V? get(K key) {
    final value = _cache.remove(key);
    if (value != null) {
      _cache[key] = value; // 移到末尾标记为最近使用
    }
    return value;
  }

  void put(K key, V value) {
    if (_cache.containsKey(key)) {
      _cache.remove(key);
    } else if (_cache.length >= capacity) {
      final lruKey = _cache.keys.first;
      final lruValue = _cache.remove(lruKey);
      if (lruValue is BetterPlayerController) {
        lruValue.pause();
        lruValue.removeEventsListener((event) {});
        lruValue.dispose(forceDispose: true);
      }
    }
    _cache[key] = value;
  }

  void remove(K key) {
    final value = _cache.remove(key);
    if (value is BetterPlayerController) {
      value.pause();
      value.removeEventsListener((event) {});
      value.dispose(forceDispose: true);
    }
  }

  void clear() {
    _cache.forEach((_, value) {
      if (value is BetterPlayerController) {
        value.pause();
        value.removeEventsListener((event) {});
        value.dispose(forceDispose: true);
      }
    });
    _cache.clear();
  }
}

2. 整合LRU缓存到视频播放逻辑

在视频Widget的State中,用LRU缓存管理控制器,同时实现了下一个、下下个视频的预加载:

class _VideoByteReelWidgetState extends State<VideoByteReelWidget> {
  BetterPlayerController? videoController;
  static final LRUCache<int, BetterPlayerController> _controllerCache = LRUCache(5); // 缓存上限设为5个控制器

  Future<void> initVideoPlayerController(String? videoUrl, {double? aspectRatio, Widget? placeHolderWidget}) async {
    if (videoUrl == null || videoController?.isVideoInitialized() == true) {
      return;
    }

    // 优先从缓存获取控制器
    videoController = _controllerCache.get(byte.id);
    if (videoController != null) {
      onVideoInitialized();
      if (mounted) setState(() {});
      return;
    }

    // 缓存无对应控制器则初始化新实例
    videoController = BetterPlayerController(
      _initBetterPlayerConfiguration(aspectRatio: aspectRatio, placeHolderWidget: placeHolderWidget),
      betterPlayerDataSource: _initBetterPlayerDataSource(videoUrl),
      betterPlayerPlaylistConfiguration: const BetterPlayerPlaylistConfiguration(),
    );

    // 将新控制器存入缓存
    _controllerCache.put(byte.id, videoController!);
    if (mounted) {
      setState(() {});
    }

    videoController!.setControlsAlwaysVisible(true);
    videoController!.setControlsEnabled(true);
    videoController!.setVolume(1.0);
    videoController?.addEventsListener(_onPlayerEvent);

    if (!(await WakelockPlus.enabled)) {
      WakelockPlus.enable();
    }

    // 预加载下一个视频
    if (widget.nextByte != null && widget.nextByte!.cdnUrl != null && nextByteIdKey != widget.nextByte?.id) {
      nextByteIdKey = widget.nextByte!.id;
      nextByteVideoController = _controllerCache.get(nextByteIdKey!);
      if (nextByteVideoController == null) {
        final nextDataSource = _initBetterPlayerDataSource(widget.nextByte!.cdnUrl!);
        nextByteVideoController = BetterPlayerController(
          _initBetterPlayerConfiguration(
            aspectRatio: computeByteAspectRation(widget.nextByte!.videoAspectRatio!),
            placeHolderWidget: _buildVideoImagePlaceHolder(widget.nextByte!.thumbCdnUrl),
          ),
          betterPlayerDataSource: nextDataSource,
          betterPlayerPlaylistConfiguration: const BetterPlayerPlaylistConfiguration(),
        );
        _controllerCache.put(nextByteIdKey!, nextByteVideoController!);
        await nextByteVideoController?.preCache(nextDataSource);
      }
    }

    // 预加载下下个视频(注:原文此处代码存在截断)
    if (widget.nextNextByte != null && widget.nextNextByte!.cdnUrl != null && nextNextByteIdKey != widget.nextNextByte?.id) {
      nextNextByteIdKey = widget.nextNextByte!.id;
      nextNextByteVideoController = _controllerCache.get(nextNextByteIdKey!);
      if (nextNextByteVideoController == null) {
        final nextNextDataSource = _initBetterPlayerDataSource(widget.nextNextByte!.cdnUrl!);
        nextNextByteVideoController = BetterPlayerController(
          _initBetterPlayerConfiguration(
            aspectRatio: computeByteAspectRation(widget.nextNextByte!.videoAspectRatio!),
            placeHolderWidget: _buildVideoImagePlaceHolder(widget.nextNextByte!.thumbCdnUrl),
          ),
          betterPlayerDataSource: nextNextDataSource,
          betterPlayerPlaylistConfiguration: const BetterPlayerPlaylistConfiguration(),
        );
        _controllerCache.put(nextNextByteIdKey!, nextNextByteVideoController!);
        await nextNextByteVideoController?.preCache(nextNextDataSource);
      }
    }
  }

  // 以下省略_onPlayerEvent、onVideoInitialized等辅助方法实现
}

想请教的问题

目前虽然用了LRU缓存,但OOM崩溃和切换延迟的问题还是没解决,想问问各位大佬:

  1. 我的LRU缓存实现有没有漏洞?比如销毁控制器的逻辑是否彻底,会不会有内存泄漏?
  2. 预加载的逻辑是否合理?比如预加载的数量、时机有没有优化空间?
  3. 有没有更成熟的BetterPlayer控制器管理方案,能有效避免内存溢出?
  4. 切换视频的延迟该从哪些方向优化?

内容来源于stack exchange

火山引擎 最新活动