Что не так с этим шаблоном AVFoundation KVO для видеоплеера [ссылка: AVPlayerLayer, AVPlayerItem, AVURLAsset]?

Я написал подкласс UIView «VideoPlayerView», чтобы инкапсулировать воспроизведение видео AVFoundation. Я полагал, что у меня есть пуленепробиваемый шаблон KVO, настроенный для наблюдения за AVPlayer, AVPlayerItems и AVURLAssets с целью загрузки, воспроизведения и обработки ошибок.

Вместо этого я нахожу сообщения о сбоях, от которых этот шаблон был специально настроен для защиты (редко, но, тем не менее, о них сообщают).

a) Экземпляр 0x170019730 класса AVPlayerItem был освобожден, хотя для него все еще были зарегистрированы наблюдатели значений ключа.

b) [VideoPlayerView setPlayerItem:] Невозможно удалить наблюдатель VideoPlayerView для ключевого пути "status" из AVPlayerItem, поскольку он не зарегистрирован в качестве наблюдателя.

c) [VideoPlayerView setAsset:] Невозможно удалить наблюдатель VideoPlayerView 0x145e3bbd0 для ключевого пути, "воспроизводимого" из AVURLAsset 0x170233780, так как он не зарегистрирован в качестве наблюдателя.

Я хотел бы узнать, почему возникают эти ошибки, что я пропустил или неправильно понял и как сделать все более надежным.

Конкретные детали упрощены для целей объяснения, но я считаю, что вся необходимая информация находится здесь.

У меня есть класс 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 реализует метод расщепления следующим образом:

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

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

Да, я должен просто встроить эту бессмысленную абстракцию! Тем не менее, это означает, что при освобождении VideoPlayerView любые сильные свойства в нем удаляются, а затем освобождаются, чтобы разрешить их освобождение.

Итак, я считаю, что этот шаблон должен смягчить сбои, которые я наблюдаю, следующим образом:

a) Экземпляр 0x170019730 класса AVPlayerItem был освобожден, хотя для него все еще были зарегистрированы наблюдатели значений ключа.

VideoPlayerView — единственный мой класс, наблюдающий за AVPlayerItem. VideoPlayerView поддерживает сильную ссылку на AVPlayerItem все время, пока наблюдает за ним. Поэтому AVPlayerItem не может быть освобожден, пока VideoPlayerView активен, и до его освобождения VideoPlayerView перестанет наблюдать за AVPlayerItem до последующего освобождения AVPlayerItem.

Как это происходит не так?

b) [VideoPlayerView setPlayerItem:] Невозможно удалить наблюдатель VideoPlayerView для ключевого пути "status" из AVPlayerItem, поскольку он не зарегистрирован в качестве наблюдателя.

c) [VideoPlayerView setAsset:] Невозможно удалить наблюдатель VideoPlayerView 0x145e3bbd0 для ключевого пути, "воспроизводимого" из AVURLAsset 0x170233780, так как он не зарегистрирован в качестве наблюдателя.

Мои пользовательские установщики пытаются удалить наблюдение за любым ранее установленным AVPlayerItem или AVURLAsset до замены свойства указателем на новый или входящий AVPlayerItem или AVURLAsset.

Когда создается экземпляр моего класса, _playerItem и _asset равны нулю. Поэтому любой предыдущий AVPlayerItem или AVURLAsset должен быть установлен с помощью пользовательского установщика, и, следовательно, VideoPlayerView должен быть зарегистрирован как наблюдатель для этих путей.

Как задаются эти свойства без настройки наблюдения?


Это просто ужасные условия гонки, основанные на порядке вызовов методов в пользовательских сеттерах?

Есть ли что-то фундаментальное, что мне здесь не хватает?

Я рассматриваю возможность использования среды выполнения target-c для создания связанного свойства объекта BOOL isObserved для этих объектов, чтобы иметь возможность выполнить проверку работоспособности перед попыткой удалить наблюдателя. Я чувствую, что даже это не будет достаточно надежным, учитывая проблемы с текущей методологией.

Любое понимание или помощь высоко ценятся. Спасибо за чтение.


person Alexander Gingell    schedule 07.07.2017    source источник
comment
Не связано с вылетами, но в setPlayerItem вы удаляете наблюдателей уведомлений из нового параметра playerItem вместо старого ивара _playerItem.   -  person Willeke    schedule 07.07.2017
comment
Спасибо, что указали на это @Willeke - очень признателен.   -  person Alexander Gingell    schedule 08.07.2017


Ответы (1)


После долгих разговоров с инженерами Apple вывод, похоже, заключается в том, что отмена регистрации наблюдения KVO в методе Dealloc класса наблюдения не является хорошей моделью. Руководство Apple по KVO рекомендует не использовать пользовательские сеттеры или геттеры в методах init и Dealloc, однако мне сказали, что язык документации должен был быть намного сильнее в этом вопросе — этого никогда не следует делать.

По сути, никогда не гарантируется его работа из-за сложности реализации KVO. Это МОЖЕТ работать в определенных случаях, но это никогда не гарантируется и демонстрирует высокую степень непредсказуемости - случайные сбои почти ожидаемы, если только случай не очень простой.

Ниже приведены некоторые избранные выдержки из моей переписки с Apple относительно этого шаблона, перефразированные для SO:

Проблема здесь заключается в широком диапазоне того, как люди взаимодействуют с KVO, и в том, как более сложные шаблоны использования меняют поведение. В простом случае подкласса NSObject, наблюдающего за другим объектом, на самом деле не так уж много проблем. Когда ситуация становится более сложной, все начинает ломаться и становится намного уродливее. Когда вы проводите много времени, разглядывая странные кейсы с краями, которые ломаются, вы становитесь гораздо более параноидальным в своем подходе.

Относительный возраст и история KVO на macOS также являются частью этого. По сравнению с iOS, приложения macOS обычно имеют гораздо более простые шаблоны подклассов — здесь нет класса ViewController, как в iOS, и они, как правило, в значительной степени полагаются на стандартные классы пользовательского интерфейса, поэтому это совсем не необычно для большинства классов в macOS. приложение для наследования непосредственно от NSObject.

По сути, проблема здесь в том, что многие простые случаи работают просто отлично, а сложные случаи… могут быть очень, очень странными. Эти проблемы известны, но тот факт, что у многих разработчиков это «просто работает» в их приложениях, означает, что они не обязательно так заметны.

Вот достойный обзор этой точки зрения: http://khanlou.com/2013/12/kvo-considered-harmful/

Подводя итог:

В идеале KVO должен устанавливаться и сбрасываться в четко определенных логических точках в жизни вовлеченных классов, а не полагаться на Dealloc везде, где это возможно. Ясно, что есть некоторые случаи, когда это невозможно — когда наблюдение должно происходить на протяжении всего жизненного цикла объекта, который может быть освобожден в неизвестной точке (т. случаях мне рекомендовали использовать отдельный класс-оболочку для обработки KVO.

Вместо того, чтобы писать свой собственный, я исследовал и решил использовать превосходный класс-оболочку PMKVObserver Лили Баллард. Это чрезвычайно удобно, потокобезопасно и автоматически обрабатывает отмену регистрации в случае смерти наблюдателя или наблюдающего объекта.

https://github.com/postmates/PMKVObserver

На момент написания статьи все эти исключения исчезли в сборке с использованием PMKVObserver вместо этого шаблона освобождения от регистрации.

person Alexander Gingell    schedule 24.09.2017