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

SwiftUI VideoPlayer内存泄漏问题(@State管理异常)

循环视频播放器的内存与状态管理问题

我尝试基于AVKit的VideoPlayer实现一个循环视频播放器视图,代码如下:

VideoPlayerView 实现

struct VideoPlayerView: View {
    let video: Video
    let isPlaying: Bool
    
    init(video: Video, isPlaying: Bool) {
        Self.logger.info("VideoPlayerView.init: \(video.name)")
        
        self.video = video
        self.isPlaying = isPlaying
        
        if let url = self.video.url {
            let playerItem = AVPlayerItem(url: url)
            self._player = State(initialValue: AVQueuePlayer(playerItem: playerItem))
            self._looper = State(initialValue: AVPlayerLooper(player: self.player!, templateItem: playerItem))
        }
    }
    
    var body: some View {
        let _ = print("VideoPlayerView body requested: “\(self.video.name)” [\(self.namespace)]", terminator: " -- ")
        let _ = Self._printChanges()
        
        VStack {
            if let player = self.player {
                VideoPlayer(player: player)
            } else {
                Text("Unable to create player.")
            }
        }
        .onAppear() {
            Self.logger.info("Player appeared: \(self.video.name)")
            self.isPlaying ? self.player?.play() : self.player?.pause()
        }
        .onDisappear() {
            Self.logger.info("Player disappeared: \(self.video.name)")
            self.player?.pause()
            self.player?.seek(to: .zero)
        }
        .onChange(of: self.isPlaying) { inOld, inNew in
            Self.logger.info("isPlaying changed: \(inNew)")
            inNew ? self.player?.play() : self.player?.pause()
        }
    }
    
    @State private var player: AVQueuePlayer?
    @State private var looper: AVPlayerLooper?
    
    @Namespace private var namespace
    
    static let logger = Logger(subsystem: "<>", category: "VideoPlayerView")
}

父视图中使用方式

作为NavigationSplitView详情页的一部分,根据选中项实例化:

detail: {
    let sortedSelection = self.selection.sorted(using: SortDescriptor(\.lastViewed))
    if sortedSelection.count > 0 {
        ScrollView {
            VStack {
                ForEach(sortedSelection) { video in
                    VideoPlayerView(video: video, isPlaying: self.isPlaying)
                        .id(video)
                        .aspectRatio(4.0/3.0, contentMode: .fit)
                }
            }
        }
    } else {
        Text("Select an Item")
    }
}

Video 数据模型

@Model
final class Video {
    var name: String
    var bookmark: Data
    var lastViewed: Date?
    
    var url: URL? {
        // 从bookmark获取URL的逻辑
    }
}

遇到的问题

  • 播放视频时切换选中项,原视频视图消失后仍能听到音频(添加onDisappear的pause()后有所改善,但未彻底解决)
  • 每次状态变更(如播放/暂停切换)都会创建新的AVFoundation对象,不确定@State在此场景下是否会丢弃初始值
  • 内存占用持续上升,每次启停视频都会创建新线程,内存无法有效释放

尝试过在onAppear中初始化状态、onDisappear中将状态置为nil,但内存增长和线程创建问题仍存在,找不到更好的播放器状态管理方式。


解决方案

1. 禁止在init中初始化@State变量

@State变量的生命周期由SwiftUI管理,手动在init中赋值_player和_looper会绕过SwiftUI的状态回收机制,导致旧的AVFoundation对象无法被正确释放,重复创建新对象。

修改方式:把播放器初始化逻辑移到onAppear,并添加判断避免重复创建:

struct VideoPlayerView: View {
    let video: Video
    let isPlaying: Bool
    
    @State private var player: AVQueuePlayer?
    @State private var looper: AVPlayerLooper?
    
    // ... 其他属性
    
    var body: some View {
        VStack {
            // ... 视图内容
        }
        .onAppear() {
            guard player == nil, let url = video.url else { return }
            let playerItem = AVPlayerItem(url: url)
            let queuePlayer = AVQueuePlayer(playerItem: playerItem)
            self.player = queuePlayer
            self.looper = AVPlayerLooper(player: queuePlayer, templateItem: playerItem)
            
            // 控制播放状态
            self.isPlaying ? queuePlayer.play() : queuePlayer.pause()
        }
        // ... 其他修饰符
    }
}

2. 彻底清理AVFoundation资源

在onDisappear中不仅要暂停播放,还要主动销毁播放器和循环器,断开所有引用,让ARC能回收底层资源:

.onDisappear() {
    player?.pause()
    player?.seek(to: .zero)
    // 销毁循环器和播放器,释放底层资源
    looper = nil
    player = nil
}

3. 用@StateObject封装播放器逻辑

把播放器的创建、播放控制、资源清理逻辑抽离到独立的ObservableObject类中,让状态管理更清晰,也能更精准地控制资源生命周期:

class VideoPlayerManager: ObservableObject {
    private(set) var player: AVQueuePlayer?
    private var looper: AVPlayerLooper?
    let video: Video
    
    init(video: Video) {
        self.video = video
    }
    
    func preparePlayer() {
        guard player == nil, let url = video.url else { return }
        let playerItem = AVPlayerItem(url: url)
        let queuePlayer = AVQueuePlayer(playerItem: playerItem)
        self.player = queuePlayer
        self.looper = AVPlayerLooper(player: queuePlayer, templateItem: playerItem)
    }
    
    func play() {
        player?.play()
    }
    
    func pause() {
        player?.pause()
        player?.seek(to: .zero)
    }
    
    func cleanup() {
        pause()
        looper = nil
        player = nil
    }
}

然后在VideoPlayerView中使用:

struct VideoPlayerView: View {
    let video: Video
    let isPlaying: Bool
    @StateObject private var playerManager: VideoPlayerManager
    
    init(video: Video, isPlaying: Bool) {
        self.video = video
        self.isPlaying = isPlaying
        self._playerManager = StateObject(wrappedValue: VideoPlayerManager(video: video))
    }
    
    var body: some View {
        VStack {
            if let player = playerManager.player {
                VideoPlayer(player: player)
            } else {
                Text("Unable to create player.")
            }
        }
        .onAppear() {
            playerManager.preparePlayer()
            isPlaying ? playerManager.play() : playerManager.pause()
        }
        .onDisappear() {
            playerManager.cleanup()
        }
        .onChange(of: isPlaying) { newValue in
            newValue ? playerManager.play() : playerManager.pause()
        }
    }
}

4. 优化父视图的资源占用

如果选中项频繁切换,可考虑限制同时存在的播放器数量——比如只给当前可见的播放器初始化资源,避免一次性创建过多AVPlayer实例,进一步降低内存和线程开销。

关键逻辑说明

  • @State变量必须由SwiftUI初始化,手动赋值会破坏状态的持久化与回收逻辑
  • AVPlayer和AVPlayerLooper持有底层音频/视频资源,必须显式置nil才能触发系统回收
  • 封装播放器逻辑到独立对象中,能更精准地控制资源的创建与销毁时机

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 08:35:06