当你开发一个视频播放应用时,可能90%的精力都花在了播放控制、UI交互这些显性功能上。但真正决定应用稳定性的,往往是那些看不见的细节——比如播放结束后的状态管理。我见过太多应用因为忽视这个问题,导致内存泄漏、UI状态错乱,甚至应用崩溃。
想象一下这样的场景:用户在刷短视频列表,每次播放结束自动跳转下一个视频。如果前一个播放器的资源没有正确释放,10个视频之后内存占用可能翻倍;如果UI状态没有重置,进度条可能卡在99%不动。这些问题不会在demo阶段暴露,但上线后随着用户量增加,就会变成性能杀手。
AV Foundation框架虽然强大,但不会自动帮你处理这些细节。它就像一辆高性能跑车,给你极致的驾驶体验,但如果你忘记加油或者保养,迟早会抛锚。播放结束后的状态管理,就是这辆跑车的定期保养。
AVPlayerItem会在播放完成时发送didPlayToEndTimeNotification通知,这是整个状态管理流程的起点。但这里有个坑:通知注册和取消必须成对出现。我遇到过最隐蔽的bug就是旋转屏幕时因为重复注册通知导致回调被执行多次。
swift复制// 正确的通知注册方式
func addPlayToEndObserver() {
guard let playerItem = player?.currentItem else { return }
NotificationCenter.default.addObserver(
self,
selector: #selector(handlePlayToEnd),
name: .AVPlayerItemDidPlayToEndTime,
object: playerItem
)
}
// 必须配套的移除操作
func removePlayToEndObserver() {
NotificationCenter.default.removeObserver(
self,
name: .AVPlayerItemDidPlayToEndTime,
object: player?.currentItem
)
}
实测发现,如果在viewWillDisappear中不移除观察者,当用户快速滑动列表时,可能出现通知回调找不到已释放对象而崩溃的情况。
根据业务场景不同,我总结出三种典型的结束处理方案:
完全释放方案:适合短视频单次播放
软重置方案:适合需要保持播放器存活的场景
swift复制func softReset() {
player?.pause()
player?.seek(to: .zero)
updateUIForPausedState()
removeTimeObserver() // 关键!
}
这个方案最容易被忽视的是时间观察者的移除。如果不移除,即使player暂停了,观察者闭包可能仍被调用。
连续播放方案:用于视频列表自动连播
replaceCurrentItem前清理前一个item的所有观察者AVFoundation开发中最常见的内存问题就是循环引用。这个检测方法我用了很多年:
如果deinit没被调用,说明有引用循环。常见陷阱包括:
swift复制// 危险写法
player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in
self.updateProgress() // 强引用self
}
// 安全写法
player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in
self?.updateProgress()
}
完整的清理流程应该包含:
特别提醒:iOS 14+系统在后台播放时,如果不手动释放资源,系统可能会强制终止你的应用。
我踩过最痛的坑是播放结束后UI显示"播放中"状态。正确的同步策略应该是:
swift复制func handlePlaybackEnd() {
DispatchQueue.main.async {
self.playButton.setImage(UIImage(named: "play"), for: .normal)
self.progressBar.setProgress(1.0, animated: true)
self.showEndOverlay() // 显示结束浮层
}
}
注意一定要在主线程更新UI,特别是在通知回调中。曾经有个诡异bug是进度条偶尔不更新,最后发现是因为在后台线程修改了UI。
播放结束不一定是自然结束,还可能是被来电、闹钟等系统事件打断。完整的处理需要监听这些事件:
swift复制// 在初始化时添加中断监听
NotificationCenter.default.addObserver(
self,
selector: #selector(handleInterruption),
name: AVAudioSession.interruptionNotification,
object: nil
)
@objc func handleInterruption(notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
if type == .began {
// 中断开始,处理暂停逻辑
pausePlayback()
} else {
// 中断结束,检查是否应该恢复播放
if let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
resumePlayback()
}
}
}
}
当启用画中画(PiP)时,状态管理会更复杂。必须实现AVPictureInPictureControllerDelegate:
swift复制func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
// 恢复UI
showPlayerControls()
completionHandler(true)
}
func pictureInPictureControllerDidStopPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
// PiP结束时清理资源
if playbackEnded {
releaseResources()
}
}
如需支持后台播放,除了设置audio session category外,还要处理:
swift复制var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
func setupBackgroundPlayback() {
backgroundTaskID = UIApplication.shared.beginBackgroundTask {
// 后台时间即将用完
self.endBackgroundTask()
}
}
func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
backgroundTaskID = .invalid
}
在实际项目中,我发现很多开发者只关注播放功能的实现,却忽视了播放结束后的状态管理这个关键环节。正确的资源释放和状态重置不仅能提升应用稳定性,还能显著降低内存占用。特别是在需要长时间运行的视频列表场景中,良好的状态管理可以让你的应用在内存压力测试中脱颖而出。