视频播放器的这个 AVFoundation KVO 模式有什么问题 [ref: AVPlayerLayer, AVPlayerItem, AVURLAsset]?

What is wrong with this AVFoundation KVO pattern for a video player [ref: AVPlayerLayer, AVPlayerItem, AVURLAsset]?

我写了一个UIView subclass "VideoPlayerView"来封装AVFoundation视频播放。我相信我设置了一个防弹 KVO 模式来处理对 AVPlayer、AVPlayerItems 和 AVURLAssets 的观察,以便加载、播放和错误处理。

相反,我发现报告的崩溃是专门为防止这种模式而设置的(很少,但仍然报告)。

a) class AVPlayerItem 的一个实例 0x170019730 被释放,而键值观察者仍然在其中注册。

b) [VideoPlayerView setPlayerItem:] 无法从 AVPlayerItem 中删除关键路径 "status" 的观察者 VideoPlayerView,因为它未注册为观察者.

c) [VideoPlayerView setAsset:] 无法从 AVURLAsset 0x170233780 中删除关键路径 "playable" 的观察者 VideoPlayerView 0x145e3bbd0,因为它未注册作为观察者。

我想了解为什么会出现这些错误,我错过或误解了什么,以及如何使事情变得更稳健。

为了解释的目的简化了具体细节,但我相信所有相关信息都在这里。

我有一个 class VideoPlayerView,它包含以下属性:

@property (strong, nonatomic) AVPlayerItem *playerItem;
@property (strong, nonatomic) AVURLAsset *asset;
@property (strong, nonatomic, readonly) AVPlayerLayer *playerLayer;

请注意,所有引用都是强引用 - 在 VideoPlayerView(正在进行观察)本身被释放之前,无法释放这些对象。 AVPlayerLayer maintains a strong reference to its AVPlayer property.

我按如下方式实现自定义 getter:

- (AVPlayer*)player
{
    return [(AVPlayerLayer*)self.layer player];
}

- (AVPlayerLayer *)playerLayer
{
    return (AVPlayerLayer *)self.layer;
}

我按如下方式实现自定义 setters:

- (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];
    }
}

在这些自定义 setter 之外,VideoPlayerView 始终使用 "self.property =" 或“[self setProperty:]”,从不使用“_属性 =”,因此自定义 setter 总是被使用。

最后VideoPlayerView实现了一个dealloc方法如下:

- (void) dealloc
{
    [self releasePlayerAndAssets];
}

- (void) releasePlayerAndAssets
{
    [self setAsset:nil];
    [self setPlayerItem:nil];
    [self setPlayer:nil];
}

是的,我应该内联这个毫无意义的抽象!然而,这意味着在取消分配 VideoPlayerView 时,其中的任何强属性都会删除其观察值,然后释放以允许其取消分配。

那么,我相信这种模式应该可以减轻我观察到的崩溃,如下所示:

a) class AVPlayerItem 的一个实例 0x170019730 被释放,而键值观察者仍然在其中注册。

VideoPlayerView 是我观察 AVPlayerItem 的唯一 class。 VideoPlayerView 在观察它的同时始终保持对 AVPlayerItem 的强引用。因此,当 VideoPlayerView 处于活动状态时,AVPlayerItem 无法被释放,并且在其释放之前,VideoPlayerView 将在 AVPlayerItem 的后续释放之前停止观察 AVPlayerItem。

这是怎么回事?

b) [VideoPlayerView setPlayerItem:] 无法从 AVPlayerItem 中删除关键路径 "status" 的观察者 VideoPlayerView,因为它未注册为观察者.

c) [VideoPlayerView setAsset:] 无法从 AVURLAsset 0x170233780 中删除关键路径 "playable" 的观察者 VideoPlayerView 0x145e3bbd0,因为它未注册作为观察者。

我的自定义 setter 试图在用指向新的或传入的 AVPlayerItem 或 AVURLAsset 的指针替换 属性 之前删除对任何先前设置的 AVPlayerItem 或 AVURLAsset 的观察。

当我的 class 实例化时,_playerItem 和 _asset 为零。因此,任何以前的 AVPlayerItem 或 AVURLAsset 都必须通过自定义 setter 进行设置,因此必须将 VideoPlayerView 注册为这些键路径的观察者。

如何在不设置观察的情况下设置这些属性?


这些只是基于自定义 setter 中方法调用顺序的可怕竞争条件吗?

我在这里缺少一些基本的东西吗?

我正在考虑使用 objective-c 运行时创建一个关联对象 属性 BOOL isObserved 在这些对象上只是为了能够在尝试删除观察者之前进行健全性检查。考虑到当前方法的问题,我觉得即使这样也不够稳健。

非常感谢任何见解或帮助。感谢阅读。

在与 Apple 工程师进行了长时间的对话之后,带走的消息似乎是在观察 class 的 dealloc 方法中注销 KVO 观察不是一个好的模式。 Apple 的 KVO 指南确实建议不要在 init 和 dealloc 方法中使用自定义 setter 或 getter,但是我被告知文档的语言在这一点上应该更强大——永远不应该这样做。

本质上,由于 KVO 实现的复杂性,它永远无法保证工作。它可能在某些情况下有效,但它永远无法保证并且显示出高度的不可预测性 - 随机崩溃几乎是可以预料的,除非情况非常简单。

我与 Apple 就此模式的通信中的一些选择摘录如下,转述为 SO:

The challenge here is the broad span of how people interact with KVO and how more complex usage patterns shift behaviors around. In the simple case of an NSObject subclass observing another object, there really isn’t much of an issue. It’s when the situation becomes more complex that things start to breakdown and get a lot uglier. When you spend a lot of time staring at the weird edges cases that break, you get a lot more paranoid in your approach.

KVO’s relative age and history on macOS is part of this as well. Compared to iOS, macOS apps generally have much simpler subclassing patterns - there is no ViewController class in the same way as iOS and they tend to rely heavily on the standard UI classes, so it’s not at all unusual for most of the classes in a macOS app to inherit directly from NSObject.

Basically, the problem here is that many simple case work just fine, and the complicated cases… can be really, really weird. These problems are not unknown, but the fact that lots of developers have had it “just work” in their app means they’re not necessarily that visible.

Here is an decent overview of that perspective: http://khanlou.com/2013/12/kvo-considered-harmful/

总结:

理想情况下,KVO 应该在所涉及的 classes 生命周期中明确定义的逻辑点上设置和取消设置,并且尽可能不要依赖 dealloc。显然,在某些情况下这是不可能的——观察必须贯穿对象的整个生命周期,该对象可以在未公开的点释放(即由 iOS 管理,例如回收的集合视图单元格) - 在那些情况下,我被建议使用单独的包装器 class 来处理 KVO。

我研究并决定使用 Lily Ballard 出色的 PMKVObserver 包装器,而不是自己编写,class。它非常方便,线程安全,并且在观察者或观察对象死亡时自动处理注销。

https://github.com/postmates/PMKVObserver

在撰写本文时,所有这些异常都已在使用 PMKVObserver 代替此 dealloc-unregistration 模式的构建中消失。