我写了一个UIView子类“VideoPlayerView”来封装AVFoundation视频播放。我相信我设置了一个防弹KVO模式来处理AVPlayer,AVPlayerItems和AVURLAssets的观察,以便加载,回放和处理错误。
相反,我发现崩溃报告说这种模式是专门为防范而设置的(很少,但仍然报告)。
a) 类AVPlayerItem的实例0x170019730已取消分配,而键值观察者仍在其中注册。
b)[VideoPlayerView setPlayerItem:] 无法从AVPlayerItem中删除观察者VideoPlayerView的关键路径“status”,因为它未注册为观察者。
c)[VideoPlayerView setAsset:] 无法从AVURLAsset 0x170233780删除观察者VideoPlayerView 0x145e3bbd0,因为它未注册为观察者。 < / p>
我想了解为什么这些错误正在发生,我错过或误解了什么,以及如何使事情更加健壮。
为了解释的目的,简化了具体细节,但我相信所有相关信息都在这里。
我有一个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;
}
我按如下方式实现自定义setter:
- (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中删除观察者VideoPlayerView的关键路径“status”,因为它未注册为观察者。
c)[VideoPlayerView setAsset:] 无法从AVURLAsset 0x170233780删除观察者VideoPlayerView 0x145e3bbd0,因为它未注册为观察者。 < / p>
在使用指向新的或传入的AVPlayerItem或AVURLAsset的指针替换属性之前,我的自定义setter尝试删除任何先前设置的AVPlayerItem或AVURLAsset的观察。
当我的类被实例化时,_playerItem和_asset为零。因此,任何先前的AVPlayerItem或AVURLAsset必须通过自定义setter设置,因此将VideoPlayerView注册为这些keypath的观察者。
在没有设置观察的情况下如何设置这些属性?
这些只是可怕的竞争条件是基于自定义设置器中方法调用的顺序吗?
我在这里缺少一些基本的东西吗?
我正在考虑使用objective-c运行时在这些对象上创建一个关联的对象属性BOOL isObserved只是为了能够在尝试删除观察者之前进行健全性检查。考虑到当前方法的问题,我觉得即使这样也不够强大。
任何见解或帮助非常感谢。谢谢你的阅读。
答案 0 :(得分:1)
在与Apple工程师进行了长时间的对话之后,带走的消息似乎是在观察类的dealloc方法中取消注册KVO观察并不是一个好的模式。 Apple的KVO指南确实建议不要在init和dealloc方法中使用自定义setter或getter,但是我被告知文档的语言应该在这一点上更加强大 - 它永远不应该完成。
基本上,由于KVO实施的复杂性,它永远不能保证工作。它可以在某些情况下工作,但它永远不会得到保证,并且显示出高度的不可预测性 - 除非情况非常简单,否则几乎可以预期随机崩溃。
我与苹果公司就这种模式的通信中的一些选择摘录如下,转述为SO:
这里面临的挑战是人们如何与KVO互动 以及更复杂的使用模式如何改变行为。在里面 NSObject子类的简单情况就是在那里观察另一个对象 真的不是什么大问题。当情况变得更多时 事情开始崩溃并且变得更加丑陋。当你 你花了很多时间盯着那些破碎的怪异边缘的情况 在你的方法中变得更加偏执。
KVO在macOS上的相对年龄和历史也是其中的一部分。 与iOS相比,macOS应用通常具有更简单的子类化 模式 - 没有与iOS和iOS相同的ViewController类 他们倾向于严重依赖标准的UI类,所以它不在 对于macOS应用程序中的大多数类来说,直接继承都是不常见的 来自NSObject。
基本上,这里的问题是许多简单的案例工作得很好, 而复杂的案例......可能真的非常奇怪。这些 问题不是未知,但很多开发人员都有这个问题 如果它在他们的应用程序中“正常工作”意味着他们不一定是那样 可见。
以下是该观点的一个不错的概述: http://khanlou.com/2013/12/kvo-considered-harmful/
总结:
理想情况下,KVO应该在相关类的生命周期中的明确定义的逻辑点处设置和取消设置,并且尽可能不依赖于dealloc。显然,在某些情况下,这是不可能的 - 观察必须在对象的整个生命周期中发生,可以在未公开的点(即由iOS管理,例如回收的集合视图单元格)中解除分配 - 并且在那些我被建议使用一个单独的包装类来处理KVO。
我研究并决定使用Kevin Ballard优秀的PMKVObserver包装类,而不是自己编写。它非常方便,线程安全,并且在观察者或观察对象死亡时自动处理注销。
https://github.com/postmates/PMKVObserver
在撰写本文时,使用PMKVObserver代替此dealloc-unregistration模式在构建中消除了所有这些异常。