2

AVFoundation のビデオ再生をカプセル化するために、UIView サブクラス「VideoPlayerView」を作成しました。AVPlayer、AVPlayerItems、および AVURLAssets の読み込み、再生、およびエラー処理の監視を処理するために、防弾 KVO パターンをセットアップしたと思います。

代わりに、このパターンが防御するために特別に設定されたというクラッシュが報告されているのを見つけました (めったにありませんが、それでも報告されています)。

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 プロパティへの強力な参照を維持します

次のようにカスタムゲッターを実装します。

- (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 は nil です。したがって、以前の AVPlayerItem または AVURLAsset はすべて、カスタム セッターを介して設定されている必要があり、VideoPlayerView がこれらのキーパスのオブザーバーとして登録されている必要があります。

これらのプロパティは、監視を設定せずにどのように設定されていますか?


これらは、カスタム セッターでのメソッド呼び出しの順序に基づく恐ろしい競合状態ですか?

ここで私が見逃している基本的なものはありますか?

object-c ランタイムを使用して、これらのオブジェクトに関連付けられたオブジェクト プロパティ BOOL isObserved を作成し、オブザーバーを削除する前にサニティ チェックを実行できるようにすることを検討しています。現在の方法論の問題を考えると、これでも十分に堅牢ではない気がします。

どんな洞察や助けも大歓迎です。読んでくれてありがとう。

4

1 に答える 1

1

Apple エンジニアとの長い会話の結果、KVO 監視を監視クラスの dealloc メソッドで登録解除するのは適切なパターンではないというメッセージが得られたようです。Apple の KVO ガイドでは、init および dealloc メソッドでカスタム セッターまたはゲッターを使用しないことを推奨していますが、ドキュメントの言語はこの点でもっと強力であるべきだと言われました。

基本的に、KVO の実装は複雑なため、動作が保証されることはありません。特定のケースでは動作する可能性がありますが、保証されることはなく、高度な予測不可能性を示します。ケースが非常に単純でない限り、ランダムなクラッシュはほとんど予想されます。

このパターンに関する Apple とのやり取りからの抜粋を、SO に言い換えて以下に示します。

ここでの課題は、人々が KVO とどのようにやり取りするか、およびより複雑な使用パターンがどのように行動を変化させるかという幅広い範囲です。NSObject サブクラスが別のオブジェクトを観察するという単純なケースでは、実際には大きな問題はありません。状況がより複雑になると、物事が崩壊し始め、より醜くなります。壊れる奇妙なエッジケースを見つめるのに多くの時間を費やすと、アプローチがより妄想的になります。

macOS での KVO の相対的な年齢と歴史もこれの一部です。iOS と比較して、macOS アプリは一般に、はるかに単純なサブクラス化パターンを持っています。iOS と同じように ViewController クラスはなく、標準の UI クラスに大きく依存する傾向があるため、macOS のほとんどのクラスではまったく珍しいことではありません。 app を NSObject から直接継承します。

基本的に、ここでの問題は、多くの単純なケースは問題なく機能し、複雑なケースは… 本当に、本当に奇妙になる可能性があることです. これらの問題は未知ではありませんが、多くの開発者が自分のアプリで「問題なく動作」しているという事実は、必ずしも目に見えるものではないことを意味します.

その観点のまともな概要は次のとおりです。 http://khanlou.com/2013/12/kvo-considered-harmful/

要約すると:

理想的には、KVO は、関連するクラスの存続期間中の明確に定義された論理ポイントで設定および設定解除する必要があり、可能な限り dealloc に依存しないようにする必要があります。明らかに、これが不可能な場合があります - 非公開の時点で割り当て解除される可能性のあるオブジェクト (つまり、リサイクルされたコレクション ビュー セルなど、iOS によって管理される) の存続期間全体にわたって監視を行う必要がある場合 - およびそれらの場合ケースでは、KVO を処理するために別のラッパー クラスを使用することをお勧めしました。

私は自分で書くのではなく、調べた結果、Lily Ballard の優れた PMKVObserver ラッパー クラスを使用することにしました。これは非常に便利で、スレッドセーフであり、オブザーバーまたは観察オブジェクトのいずれかが死亡すると、自動的に登録解除を処理します。

https://github.com/postmates/PMKVObserver

執筆時点では、これらの例外はすべて、この割り当て解除と登録解除パターンの代わりに PMKVObserver を使用したビルドで消失しています。

于 2017-09-24T18:20:05.797 に答える