我写了一个 UIView 子类“VideoPlayerView”来封装 AVFoundation 视频播放。我相信我设置了一个防弹的 KVO 模式来处理对 AVPlayer、AVPlayerItems 和 AVURLAssets 的观察,以达到加载、播放和错误处理的目的。
相反,我发现报告的崩溃是专门为防止这种模式而设置的(很少,但仍然报告)。
a) AVPlayerItem 类的实例 0x170019730 被释放,而键值观察者仍向其注册。
b) [VideoPlayerView setPlayerItem:] 无法从 AVPlayerItem 中移除关键路径“status”的观察者 VideoPlayerView,因为它没有注册为观察者。
c) [VideoPlayerView setAsset:] 无法从 AVURLAsset 0x170233780 中删除关键路径“可播放”的观察者 VideoPlayerView 0x145e3bbd0,因为它没有注册为观察者。
我想了解为什么会发生这些错误,我错过或误解了什么,以及如何使事情变得更健壮。
为了解释的目的,对具体细节进行了简化,但我相信所有相关信息都在这里。
我有一个类 VideoPlayerView,它包含以下属性:
@property (strong, nonatomic) AVPlayerItem *playerItem;
@property (strong, nonatomic) AVURLAsset *asset;
@property (strong, nonatomic, readonly) AVPlayerLayer *playerLayer;
请注意,所有引用都是强引用 - 在 VideoPlayerView(正在执行观察)本身被释放之前,这些对象不能被释放。 AVPlayerLayer 保持对其 AVPlayer 属性的强引用。
我实现自定义getter如下:
- (AVPlayer*)player
{
return [(AVPlayerLayer*)self.layer player];
}
- (AVPlayerLayer *)playerLayer
{
return (AVPlayerLayer *)self.layer;
}
我实现自定义设置器如下:
- (void) setPlayer:(AVPlayer*)player
{
// Remove observation for any existing player
AVPlayer *oldPlayer = [self player];
[oldPlayer removeObserver:self forKeyPath:kStatus];
[oldPlayer removeObserver:self forKeyPath:kCurrentItem];
// Set strong player reference
[(AVPlayerLayer*)[self layer] setPlayer:player];
// Add observation for new player
[player addObserver:self forKeyPath:kStatus options:NSKeyValueObservingOptionNew context:kVideoPlayerViewKVOContext];
[player addObserver:self forKeyPath:kCurrentItem options:NSKeyValueObservingOptionNew context:kVideoPlayerViewKVOContext];
}
- (void) setAsset:(AVURLAsset *)asset
{
// Remove observation for any existing asset
[_asset removeObserver:self forKeyPath:kPlayable];
// Set strong asset reference
_asset = asset;
// Add observation for new asset
[_asset addObserver:self forKeyPath:kPlayable options:NSKeyValueObservingOptionNew context:kVideoPlayerViewKVOContext];
}
- (void) setPlayerItem:(AVPlayerItem *)playerItem
{
// Remove observation for any existing item
[_playerItem removeObserver:self forKeyPath:kStatus];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
[nc removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:_playerItem];
[nc removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:_playerItem];
// Set strong playerItem reference
_playerItem = playerItem;
// Add observation for new item
[_playerItem addObserver:self forKeyPath:kStatus options:NSKeyValueObservingOptionNew context:kVideoPlayerViewKVOContext];
if (_playerItem)
{
[nc addObserver:self selector:@selector(handlePlayerItemDidReachEndTimeNotification:) name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
[nc addObserver:self selector:@selector(handlePlayerItemFailureNotification:) name:AVPlayerItemPlaybackStalledNotification object:_playerItem];
[nc addObserver:self selector:@selector(handlePlayerItemFailureNotification:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:_playerItem];
}
}
在这些自定义设置器之外,VideoPlayerView 始终使用“self.property =”或“[self setProperty:]”,而从不使用“_property =”,因此始终使用自定义设置器。
最后,VideoPlayerView 实现了一个 dealloc 方法,如下所示:
- (void) dealloc
{
[self releasePlayerAndAssets];
}
- (void) releasePlayerAndAssets
{
[self setAsset:nil];
[self setPlayerItem:nil];
[self setPlayer:nil];
}
是的,我应该内联这个毫无意义的抽象!尽管如此,这意味着在解除分配 VideoPlayerView 时,其中的任何强属性都会删除其观察结果,然后将其释放以允许解除分配。
那么,我相信这种模式应该可以减轻我观察到的崩溃,如下所示:
a) AVPlayerItem 类的实例 0x170019730 被释放,而键值观察者仍向其注册。
VideoPlayerView 是我唯一观察 AVPlayerItem 的类。VideoPlayerView 在观察它的同时保持对 AVPlayerItem 的强引用。因此,当 VideoPlayerView 处于活动状态时,无法释放 AVPlayerItem,并且在其释放之前,VideoPlayerView 将在 AVPlayerItem 的后续释放之前停止观察 AVPlayerItem。
这是怎么回事?
b) [VideoPlayerView setPlayerItem:] 无法从 AVPlayerItem 中移除关键路径“status”的观察者 VideoPlayerView,因为它没有注册为观察者。
c) [VideoPlayerView setAsset:] 无法从 AVURLAsset 0x170233780 中删除关键路径“可播放”的观察者 VideoPlayerView 0x145e3bbd0,因为它没有注册为观察者。
我的自定义设置器试图在用指向新的或传入的 AVPlayerItem 或 AVURLAsset 的指针替换属性之前删除对任何先前设置的 AVPlayerItem 或 AVURLAsset 的观察。
当我的类被实例化时,_playerItem 和 _asset 为零。因此,任何以前的 AVPlayerItem 或 AVURLAsset 都必须通过自定义设置器进行设置,因此将 VideoPlayerView 注册为这些键路径的观察者。
在没有设置观察的情况下如何设置这些属性?
这些只是基于自定义设置器中方法调用顺序的可怕竞争条件吗?
我在这里缺少一些基本的东西吗?
我正在考虑使用objective-c运行时在这些对象上创建一个关联的对象属性BOOL isObserved,以便能够在尝试删除观察者之前进行完整性检查。考虑到当前方法的问题,我觉得即使这也不够稳健。
非常感谢任何见解或帮助。感谢您的阅读。