2

我写了一个 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,以便能够在尝试删除观察者之前进行完整性检查。考虑到当前方法的问题,我觉得即使这也不够稳健。

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

4

1 回答 1

1

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

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

以下是我与 Apple 关于此模式的通信中的一些选择摘录,转述为 SO:

这里的挑战是人们如何与 KVO 交互以及更复杂的使用模式如何改变行为的广泛范围。在 NSObject 子类观察另一个对象的简单情况下,实际上并没有太大问题。当情况变得更加复杂时,事情开始崩溃并变得更加丑陋。当你花大量时间盯着那些奇怪的边缘案例打破时,你的方法会变得更加偏执。

KVO 在 macOS 上的相对年龄和历史也是其中的一部分。与 iOS 相比,macOS 应用程序通常具有更简单的子类化模式 - 没有与 iOS 相同的 ViewController 类,并且它们往往严重依赖标准 UI 类,因此对于 macOS 中的大多数类来说这并不罕见应用程序直接从 NSObject 继承。

基本上,这里的问题是许多简单的案例都可以正常工作,而复杂的案例……可能真的,真的很奇怪。这些问题并不为人所知,但许多开发人员在他们的应用程序中让它“正常工作”这一事实意味着它们不一定那么明显。

这是对该观点的一个不错的概述:http: //khanlou.com/2013/12/kvo-considered-harmful/

总而言之:

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

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

https://github.com/postmates/PMKVObserver

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

于 2017-09-24T18:20:05.797 回答