Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Преодолеваем спрятанные угрозы KVO в Objective C

Anna | 2.07.2014 | нет комментариев
The major difference between a thing that might go wrong and a thing that cannot possibly go wrong is that when a thing that cannot possibly go wrong goes wrong it usually turns out to be impossible to get at or repair.
— Douglas Adams
Objective C существует теснее с 1983 года и является ровесником C . Впрочем, в различие от последнего он начал приобретать знаменитость только в 2008 году, позже выхода iOS 2.0 — новой версии операционной системы для революционного iPhone, включавшей приложение AppStore, дозволяющее пользователям приобретать приложения, создаваемые сторонними разработчиками.
Последующий триумф Objective C обеспечивался не только знаменитость устройств на базе iOS и относительной легкостью продаж через AppStore, но и существенными усилиями компании Apple по улучшению как стандартных библиотек, так и самого языка.
Согласно рейтингу TIOBE к началу 2013 года Objective C обогнал по популярности C и занял третье место, уступая только C и Java.

На сегодняшний день Objective C включает и такие касательно ветхие функции как KVC и KVO, существовавшие еще за 4 года до выхода первого iPhone, и такие новые вероятности как блоки (blocks, появившиеся в Mac OS 10.6 и iOS 4) и механический подсчет ссылок (ARC, доступный в Mac OS 10.7 и iOS 5), которые разрешают с легкостью решать задачи, вызывавшие важные сложности ранее.

KVO — это спецтехнология, дозволяющая немедленно реагировать в одном объекте (наблюдателе) на метаморфозы состояния иного объекта (отслеживаемого), без внесения познаний о типе наблюдателя в реализации отслеживаемого объекта. В Objective C, наравне с KVO, существует несколько методов решения этой задачи:

1. Делегирование — общеизвестный паттерн объектно-ориентированного программирования, состоящий в том, что объекту передается ссылка на произвольный объект (называемый делегатом), реализующий определенныйпротокол — фиксированный комплект селекторов. Позже этого реализация объекта «вручную» посылает своему делегату соответствующие случаю сообщения. Скажем, UIScrollView информирует своего делегата об изменении значения своего свойства contentOffset, вызывая селектор scrollViewDidScroll:.
Рекомендуется одним из параметров всех селекторов протокола делать ссылку на сам дерзкий его объект, Дабы в случае, когда один и тот же объект является делегатом нескольких объектов одного класса, иметь вероятность различить, от кого из них пришло сообщение.

2. Target-action. Различие этой техники от делегирования заключается в том, что взамен реализации «делегатом» определенного протокола, совместно с ним передается его селектор, тот, что и будет вызван при определенном событии. Эта техника Почаще каждого применяется преемниками UIControl, скажем, объектуUISwitch дозволено задать пару target-action для вызова при переключении этого контрола пользователем (событии UIControlEventValueChanged). Такое решение больше комфортно, нежели делегирование, в случае, когда один объект-«цель» должен реагировать на идентичные события от различных источников (скажем, нескольких UISwitch).

3. Callback block. Это решение состоит в том, что отслеживаемому объекту передается ссылка не на сам объект-наблюдатель, а на блок. Как правило данный блок создается в том же месте, где и устанавливается. При этом реализация блока способна захватывать значения локальных переменных того scope, где он определен, освобождая _permark!} – (void)setDocument:(ETRDocument *)document { [self stopObservingIsFavorite]; _document = document; [self startObservingIsFavorite]; self.titleLabel.text = self.document.title; [self updateIsFavoriteButton]; } – (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { [self updateIsFavoriteButton]; }
Запускаем — всё работает, ячейка реагирует на метаморфоза isFavorite. Мы даже можем убрать вызов updateIsFavoriteButton из toggleIsFavorite. Впрочем стоит закрыть таблицу и изменить значение isFavorite у одного из документов, как приложение падает с EXC_BAD_ACCESS.
Что же случилось? Испробуем включить NSZombieEnabled и повторить действия. На данный раз мы получаем больше осмысленное сообщение при падении:
*** -[ETRDocumentCell retain]: message sent to deallocated instance 0x8bcda20

Подлинно, заглянув в документацию по KVO мы увидим следующее:
Note: The key-value observing addObserver:forKeyPath:options:context: method does not maintain strong references to the observing object, the observed objects, or the context. You should ensure that you maintain strong references to the observing, and observed, objects, and the context as necessary.

Слежение не создает крепких ссылок ни на наблюдателя, ни на отслеживаемый объект, ни на контекст. Впрочем документация умалчивает о том, что же произойдет, когда один из этих объектов будет удален.

Контекст для KVO является обыкновенным указателем из языка C. Даже если он указывает на объект Objective C, KVO не будет рассматривать его как таковой: не будет слать ему сообщений либо отслеживать его время жизни. Следственно, если контекст будет удален, то в observeValueForKeyPath будет передана «висячая» ссылка, и попытка передать по ней сообщение приведет к итогам, аналогичным тем, которые имеем мы. Впрочем мы не применяли в нашем примере контекст. Больше того, дальше станет ясно, что контекст имеет несколько иное «правдивое» призвание.

Если удаленным окажется отслеживаемый объект, то взамен того, Дабы перестать слежение (чай никакие значения меняться огромнее не могут), в консоль будет выведено предупреждение:

An instance 0xac62490 of class ETRDocument was deallocated while key value observers were still registered
with it. Observation info was leaked, and may even become mistakenly attached to some other object.
Set a breakpoint on NSKVODeallocateBreak to stop here in the debugger. Here’s the current observation info:
<NSKeyValueObservationInfo 0xaaa77e0> (
<NSKeyValueObservance 0xaaa77a0: Observer: 0xaaa2100, Key path: isFavorite, Options:
<New: NO, Old: NO, Prior: NO> Context: 0×0, Property: 0xabf12e0>
)

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

Если же будет удален наблюдатель, KVO сохранит «висячую» ссылку на него (что соответствует модификатору unsafe_unretained в терминологии ARC), и будет посылать по ней сообщения при изменениях. Именно это и происходит в нашем примере. Допустимо, в последующих версиях поведение «unsafe_unretained» будет заменено на больше неопасное «weak», и «висячие» ссылки на наблюдателей будут механически обнуляться.
Дабы поправить это падение, довольно вызвать stopObservingIsFavorite из dealloc.

Существует метод несколько упростить логику нашей ячейки. Взамен слежения по key path «isFavorite» документа, ячейка может следить key path «document.isFavorite» на самой себе. В итоге ячейка будет оповещена как при изменении признака isFavorite в связанном документа, так и при изменении своей ссылки на документ. При этом по-бывшему нужно вызывать removeObserver из dealloc, но не необходимо прекращать и начинать слежение всякий раз при смене нынешнего документа.
Дозволено пойти дальше, и следить не только isFavorite, но и title. Это избавит нас от переопределения setDocument:, но столкнет с еще одним неудобством KVO:

@implementation ETRDocumentCell
- (void)awakeFromNib
{
    [super awakeFromNib];
    [self addObserver:self
           forKeyPath:@"document.isFavorite"
              options:0
              context:NULL];
    [self addObserver:self
           forKeyPath:@"document.title"
              options:0
              context:NULL];
}
- (void)dealloc
{
    [self removeObserver:self
              forKeyPath:@"document.isFavorite"];
    [self removeObserver:self
              forKeyPath:@"document.title"];
}
- (void)toggleIsFavorite
{
    self.document.isFavorite = !self.document.isFavorite;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if ([keyPath isEqualToString:@"document.isFavorite"]) {
        self.isFavoriteButton.selected = self.document.isFavorite;
    } else if ([keyPath isEqualToString:@"document.title"]) {
        self.titleLabel.text = self.document.title;
    }
}
@end

Ветхий (но не дюже добросердечный) разбор случаев в одном способе со сопоставлением строк, продублированных в 2-х других местах. Это не только уродливо, но и чревато ошибками, как и любая иная «копипаста».

На этом дозволено бы и остановиться, понадеявшись, что ничего плохого не произойдет, и всё будет трудиться. И теперь оно подлинно будет трудиться. Но рано либо поздно что-то дрянное все-таки может случиться, и позже пары часов в отладчике мы укрепимся в убеждении, что с KVO отменнее не связываться.
Что же может случиться? Немножко усложним наш пример, и представим, что мы решили сделать еще одну таблицу для отображения наших документов, но с чуть больше «навороченными» ячейками, которые так же будут содержать заголовок документа и такую же кнопку, но наравне с другими изменениями будут менять цвет фона в зависимости от того, является ли документ избранным.
Дабы теснее проделанная работа не пропала безвозмездно, мы решаем унаследовать новую ячейку от ветхой.
А Дабы менять фон ячейки, мы используем ту же технику KVO:

@implementation ETRAdvancedDocumentCell
- (void)awakeFromNib
{
    [super awakeFromNib];
    [self addObserver:self
           forKeyPath:@"document.isFavorite"
              options:0
              context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    [self updateBackgroundColor];
}
...

Отменно, фон меняет цвет. Вот только кнопка перестала выдаваться, заголовок перестал обновляться, да и updateBackgroundColor вызывается как-то уж слишком Зачастую. Видимо, ETRAdvancedDocumentCell получает сообщения observeValueForKeyPath, относящиеся как к собственному слежению, так и к слежениям ETRDocumentCell. Что же на данный счет написано в документации? В комментарии внутри кода одного из примеров находим следующие строки:
Be sure to call the superclass’s implementation *if it implements it*.
NSObject does not implement the method.

Мы, безусловно же, знаем, что ETRDocumentCell реализует observeValueForKeyPath, а значит необходимо вызывать
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context] из ETRAdvancedDocumentCell.

Но вызовом реализации из родительского класса всё не ограничивается. Следует обработать метаморфозы, на которые подписан сам ETRAdvancedDocumentCell, и передать родительскому классу только прочие метаморфозы. Видимо, одними проверками значений keyPath и object не обойтись: родительский класс подписан на верно тот же самый keyPath (document.isFavorite) того же самого объекта (self). Именно тут проявляется то самое «правдивое» призвание довода context.

static void* ETRAdvancedDocumentCellIsFavoriteContext = &ETRAdvancedDocumentCellIsFavoriteContext;
@implementation ETRAdvancedDocumentCell
- (void)awakeFromNib
{
    [super awakeFromNib];
    [self addObserver:self
           forKeyPath:@"document.isFavorite"
              options:0
              context:ETRAdvancedDocumentCellIsFavoriteContext];
}
- (void)dealloc
{
    [self removeObserver:self
              forKeyPath:@"document.isFavorite"
                 context:ETRAdvancedDocumentCellIsFavoriteContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == ETRAdvancedDocumentCellIsFavoriteContext) {
        [self updateBackgroundColor];
    } else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}
...

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

Видимо, что прекращать слежение следует тоже с указанием контекста. Любознателен тот факт, что соответствующий способ был добавлен только в iOS 5, и до этого существовал только вариант без довода context. Это делало немыслимым правильное прерывание одного из неотличимых по прочим параметрам слежений.

Но как же быть с ETRDocumentCell: необходимо ли вызывать super из него? Реализует ли класс UITableViewCell селектор observeValueForKeyPath? Дозволено прибегнуть к способу проб и ошибок, попытаться вызвать super, получить ожидаемое падение с исключением

*** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’,
reason: ‘<ETRDocumentCell: 0x8d3c540; baseClass = UITableViewCell;
frame = (0 0; 320 64); autoresize = W; layer = <CALayer: 0x8d3c730>>:
An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: document.title
Observed object: <ETRDocumentCell: 0x8d3c540; baseClass = UITableViewCell;
frame = (0 0; 320 64); autoresize = W; layer = <CALayer: 0x8d3c730>>
Change: {
kind = 1;
}
Context: 0×0′
*** First throw call stack:
(
0 CoreFoundation 0x0173b5e4 __exceptionPreprocess 180
1 libobjc.A.dylib 0x014be8b6 objc_exception_throw 44
2 CoreFoundation 0x0173b3bb [NSException raise:format:] 139
3 Foundation 0x0118863f -[NSObject(NSKeyValueObserving) observeValueForKeyPath:ofObject:change:context:] 94
4 ETRKVO 0x00002e35 -[ETRDocumentCell observeValueForKeyPath:ofObject:change:context:] 229
5 Foundation 0x0110d8c7 NSKeyValueNotifyObserver 362
6 Foundation 0x0110f206 NSKeyValueDidChange 458

и убрать вызов обратно. Но где ручательство того, что родительский класс не начнет (либо напротив перестанет) реализовывать observeValueForKeyPath в дальнейшей версии? Даже если родительский класс реализуете вы сами, вы рискуете позабыть добавить либо убрать вызов super в дочерних классах. Особенно верным решением было бы исполнять соответствующую проверку во время исполнения. Делается это совсем не с поддержкой вызова [super respondsToSelector:...], тот, что неизменно вернет YES, так как наш класс не переопределяет respondsToSelector:, и вызывать его на super — все равно, что вызывать на self. Делается это с поддержкой чуть больше длинного выражения [[ETRDocumentCell superclass] instancesRespondToSelector:…]. Но как выясняется, документация нас врет, и [[NSObject class] instancesRespondToSelector:selector(observeValueForKeyPath:ofObject:change:context:)] возвращает YES, при чем соответствующая реализация как раз таки и ответственна за приведенное выше исключение. Выходит, что у нас есть два варианта: либо никогда не вызывать super и рисковать сломать логику родительского класса, либо вызывать super только для слежений, гарантированно не вызванных наших кодом, рискуя получить исключение, пропустив что-нибудь лишнее.

static void* ETRDocumentCellIsFavoriteContext = &ETRDocumentCellIsFavoriteContext;
static void* ETRDocumentCellTitleContext = &ETRDocumentCellTitleContext;
@implementation ETRDocumentCell
- (void)awakeFromNib
{
    [super awakeFromNib];
    [self addObserver:self
           forKeyPath:@"document.isFavorite"
              options:0
              context:ETRDocumentCellIsFavoriteContext];
    [self addObserver:self
           forKeyPath:@"document.title"
              options:0
              context:ETRDocumentCellTitleContext];
}
- (void)dealloc
{
    [self removeObserver:self
              forKeyPath:@"document.isFavorite"
                 context:ETRDocumentCellIsFavoriteContext];
    [self removeObserver:self
              forKeyPath:@"document.title"
                 context:ETRDocumentCellTitleContext];
}
- (void)toggleIsFavorite
{
    self.document.isFavorite = !self.document.isFavorite;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == ETRDocumentCellIsFavoriteContext) {
        self.isFavoriteButton.selected = self.document.isFavorite;
    } else if (context == ETRDocumentCellTitleContext) {
        self.titleLabel.text = self.document.title;
    } else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}
@end

Из приведенного примера следует, что для правильной реализации KVO нужно сделать уйма нетривиальных и неочевидных действий. При чем они обязаны быть сделаны консистентным образом на всех ярусах наследования, что не находится во власти разработчика, если какие-то из этих ярусов реализованы в стандартных либо сторонних библиотеках, либо если сам продукт является библиотекой, полагающей наследование от некоторых из ее классов.
Помимо того, программисту нужно Отчетливо отслеживать в классе наблюдателя все энергичные слежения, Дабы гарантированно обработать их в observeValueForKeyPath и остановить их в необходимый момент (скажем, при удалении наблюдателя). Это осложняется разнесенность связанного кода по нескольким местам (определение контекстов, добавление, удаление и обработка слежений) и усугубляется тем фактом, что проверить присутствие слежения немыслимо, а попытка остановить несуществующее слежение приводит к исключению:

*** Terminating app due to uncaught exception ‘NSRangeException’,
reason: ‘Cannot remove an observer <ETRAdvancedDocumentCell 0x1566cdd0>
for the key path «document.title» from <ETRAdvancedDocumentCell 0x1566cdd0>
because it is not registered as an observer.’

Нередко дозволено встретить UIViewController’ы, добавляющие себя в качестве наблюдателя внутри реализации одного из способов viewDidLoad, vewDidUnload, viewWillAppear, viewDidAppear, viewWillDisappear либо viewDidDisappear, и прекращающие слежения в ином из этих способов. При этом никто не гарантирует суровую парность этих вызовов, исключительно при применении custom container view controllers, исключительно с shouldAutomaticallyForwardAppearanceMethods, возвращающим NO. В частности, логика этих вызовов для контроллеров, содержащихся в стеке UINavigationController, изменилась в iOS 7 с вступлением интерактивного жеста перехода назад по стеку навигации. Да и ссылка на объект, передаваемый в качестве отслеживаемого, может измениться между этими вызовами.
В итоге некоторые разработчики даже серьезно предлагают применять решения как бы дальнейшего:

@try {
    [self.document removeObserver:self
                       forKeyPath:@"isFavorite"
                          context:DocumentCellIsFavoriteContext];
}
@catch (NSException *exception) {}

Когда я вижу кое-что сходственное, я припоминаю, как в детстве я писал на Visual Basic строчку «On Error Resume Next», и мои «творения» Удивительным образом переставали падать.

Из каждого написанного следует, что KVO — дюже сильная спецтехнология, доступ к которой мы имеем посредством API, тот, что не только неудобен, но и губительно опасен для использующих его приложений. Сходственные обстановки не редки в сфере программирования, и правильным выходом из них служит написание больше комфортного и неопасного интерфейса, изолирующего и нейтрализующего все недочеты внутри своей реализации.

В случае KVO корень задач как с наследованием, так и с removeObserver, заключается в том, что отдельно взятое слежение теряет для программиста свою идентичность позже его добавления. Взамен того, Дабы прекращать «определенно вот это слежение», разработчик оказывается вынужден требовать перестать «какое-нибудь слежение, соответствующее указанным критериям». При этом таких слежений может быть несколько, либо не быть совсем. То же самое происходит и в реализации observeValueForKeyPath: когда неудовлетворительно различать слежения по объекту и ключу, доводится прибегать к специфическим контекстам. Но даже контекст определяет не определенный акт добавления слежения, а каждого лишь строку кода, в которой он совершается. Если одна и та же строка кода будет вызвана два раза с теми же наблюдателем, отслеживаемым объектом и key path, невозможно будет различить итоги этих 2-х вызовов. Аналогичным образом, задачи при наследовании вызываются еще и тем, что родительский и дочерний классы оказываются связаны в деталях реализации ими KVO (которые обязаны быть верно инкапсулированы), от того что их объект — это один и тот же наблюдатель с точки зрения KVO.

Из этих рассуждений следует, что для больше верного применения KVO нужно придать идентичность всякому отдельно взятому слежению, а именно создавать для всякого слежения обособленный объект. Данный же объект должен являться наблюдателем в терминах стандартного интерфейса KVO. Отслеживая ровно один keyPath ровно одного объекта, и Отчетливо объединяя это слежение с собственным временем жизни, данный объект будет верно защищен от описанных выше опасностей. Получая сообщение об изменении отслеживаемого значения, исключительное, что он будет делать, — это информировать иной объект одним из первых 3 указанных в начале статьи способов.
Испробуем реализовать такой объект:

@interface ETRKVO : NSObject
@property (nonatomic, unsafe_unretained, readonly) id subject;
@property (nonatomic, copy, readonly) NSString *keyPath;
@property (nonatomic, copy) void (^block)(ETRKVO *kvo, NSDictionary *change);
- (id)initWithSubject:(id)subject
              keyPath:(NSString *)keyPath
              options:(NSKeyValueObservingOptions)options
                block:(void (^)(ETRKVO *kvo, NSDictionary *change))block;
- (void)stopObservation;
@end

static void* ETRKVOContext = &ETRKVOContext;
@implementation ETRKVO
- (id)initWithSubject:(id)subject
              keyPath:(NSString *)keyPath
              options:(NSKeyValueObservingOptions)options
                block:(void (^)(ETRKVO *kvo, NSDictionary *change))block
{
    self = [super init];
    if (self) {
        _subject = subject;
        _keyPath = [keyPath copy];
        _block = [block copy];
        [subject addObserver:self
                  forKeyPath:keyPath
                     options:options
                     context:ETRKVOContext];
    }
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == ETRKVOContext) {
        if (self.block)
            self.block(self, change);
    } // NSObject does not implement observeValueForKeyPath
}
- (void)stopObservation
{
    [self.subject removeObserver:self
                      forKeyPath:self.keyPath
                         context:ETRKVOContext];
    _subject = nil;
}
- (void)dealloc
{
    [self stopObservation];
}
@end

Альтернативные решения дозволено обнаружить в библиотеке ReactiveCocoa, претендующей на коренной сдвиг парадигмы программирования на Objective C, и в несколько устаревшем MAKVONotificationCenter.
Помимо того, схожие метаморфозы по тем же причинами были сделаны в NSNotificationCenter: в iOS 4 был добавлен способ addObserverForName:object:queue:usingBlock:, возвращающий объект, идентифицирующий подписку на оповещения.

Интерфейс ETRKVO дозволено несколько упростить, разглядев поведение доводов options и change.
NSKeyValueObservingOptions является битовой маской, которая может объединять следующие флаги:

  • NSKeyValueObservingOptionNew
  • NSKeyValueObservingOptionOld
  • NSKeyValueObservingOptionInitial
  • NSKeyValueObservingOptionPrior

Первые два указывают на то, что в доводе change обязаны присутствовать ветхое и новое значения отслеживаемого признака. Никаких негативных последствий это вызывать не может, если не считать незначительное замедление.
Указание NSKeyValueObservingOptionInitial приводит к тому, что observeValueForKeyPath будет вызван сразу же при добавлении слежения, что, вообще говоря, непотребно.
Указание NSKeyValueObservingOptionPrior приводит к тому, что observeValueForKeyPath будет вызван не только позже метаморфозы значения, но и перед ним. При этом новое значение передано не будет, даже если указан флаг NSKeyValueObservingOptionNew. Надобность в этом дозволено встретить весьма редко, и скорее каждого она появляется только в процессе реализации какого-нибудь «костыля».
Следственно, дозволено неизменно передавать в качестве опций (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld).

Довод (NSDictionary *)change может содержать следующие ключи:

  • NSKeyValueChangeNewKey
  • NSKeyValueChangeOldKey
  • NSKeyValueChangeKindKey
  • NSKeyValueChangeIndexesKey
  • NSKeyValueChangeNotificationIsPriorKey

Первые два содержат те самые ветхое и новое значения, которые дозволено запросить соответствующими опциями. Значения скалярных типов оборачиваются в NSNumber либо NSValue, а взамен nil передается синглтонный объект [NSNull null].
Следующие два необходимы только при слежении за мутабельной коллекцией, что скорее каждого является дрянной идеей.
Конечный ключ передается только при предшествующем изменению вызове, исполняемом при наличии опции NSKeyValueObservingOptionPrior.
Следственно, дозволено рассматривать только ключи NSKeyValueChangeNewKey и NSKeyValueChangeOldKey, и передавать блоку их значения в развернутом виде.
Таким образом, ETRKVO дозволено изменить дальнейшим образом:

- (id)initWithSubject:(id)subject
              keyPath:(NSString *)keyPath
                block:(void (^)(ETRKVO *kvo, id oldValue, id newValue))block
{
    self = [super init];
    if (self) {
        _subject = subject;
        _keyPath = [keyPath copy];
        _block = [block copy];
        [subject addObserver:self
                  forKeyPath:keyPath
                     options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
                     context:ETRKVOContext];
    }
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == ETRKVOContext) {
        if (self.block) {
            id oldValue = change[NSKeyValueChangeOldKey];
            if (oldValue == [NSNull null])
                oldValue = nil;
            id newValue = change[NSKeyValueChangeNewKey];
            if (newValue == [NSNull null])
                newValue = nil;
            self.block(self, oldValue, newValue);
        }
    } // NSObject does not implement observeValueForKeyPath
}

При желании его дозволено оформить в виде категории над NSObject, избавившись от первого довода:

- (ETRKVO *)observeKeyPath:(NSString *)keyPath
                 withBlock:(void (^)(ETRKVO *kvo, id oldValue, id newValue))block;

От того что Зачастую keyPath является наименованием property, совпадающим с соответствующим getter’ом, взамен строки keyPath комфортнее применять селектор этого getter’а. При этом будет трудиться autocompletion, и поменьше будет вероятность допустить ошибку при написании, либо при переименовании property.

- (ETRKVO *)observeSelector:(SEL)selector
                  withBlock:(void (^)(ETRKVO *kvo, id oldValue, id newValue))block
{
    return [[ETRKVO alloc] initWithSubject:self
                                   keyPath:NSStringFromSelector(selector)
                                     block:block];
}

Перепишем наши ячейки с применением этого класса и категории

@interface ETRDocumentCell ()
@property (nonatomic, strong) ETRKVO* isFavoriteKVO;
@property (nonatomic, strong) ETRKVO* titleKVO;
@end

@implementation ETRDocumentCell
- (void)awakeFromNib
{
    [super awakeFromNib];
    typeof(self) __weak weakSelf = self;
    self.isFavoriteKVO = [self observeKeyPath:@"document.isFavorite"
                                    withBlock:^(ETRKVO *kvo, id oldValue, id newValue)
                          {
                              weakSelf.isFavoriteButton.selected = weakSelf.document.isFavorite;
                          }];
    self.titleKVO = [self observeKeyPath:@"document.title"
                               withBlock:^(ETRKVO *kvo, id oldValue, id newValue)
                     {
                         weakSelf.titleLabel.text = weakSelf.document.title;
                     }];
}
- (void)dealloc
{
    [self.isFavoriteKVO stopObservation];
    [self.titleKVO stopObservation];
}
- (void)toggleIsFavorite
{
    self.document.isFavorite = !self.document.isFavorite;
}
@end

@interface ETRAdvancedDocumentCell ()
@property (nonatomic, strong) ETRKVO* advancedIsFavoriteKVO;
@end

@implementation ETRAdvancedDocumentCell
- (void)awakeFromNib
{
    [super awakeFromNib];
    typeof(self) __weak weakSelf = self;
    self.advancedIsFavoriteKVO = [self observeKeyPath:@"document.isFavorite"
                                            withBlock:^(ETRKVO *kvo, id oldValue, id newValue)
                                  {
                                      [weakSelf updateBackgroundColor];
                                  }];
}
- (void)dealloc
{
    [self.advancedIsFavoriteKVO stopObservation];
}
...

Полную реализацию ETRKVO совместно с примером дозволено скачать тут

Исключительным неочевидным приемом тут является применение weakSelf для предотвращения утрат памяти. Если бы блоки захватывали self по мощной ссылке, образовывался бы цикл крепких ссылок: ETRDocumentCell нологией приводит к явлению, называемому «[Имя технологии] hell». Хоть связи между объектами, создаваемые с поддержкой KVO, и выглядят дюже слабыми, выйдя из-под контроля, они могут дюже больно стукнуть. В нашем случае «KVO hell» может выражаться в непредсказуемых лавинообразных срабатываниях обработчиков слежений, приводящих к непредвиденным итогам и убивающих продуктивность, либо даже в циклических вызовах, завершающихся переполнением стека.

  1. TIOBE Programming Community Index for November 2013
  2. Key-Value Coding Programming Guide
  3. Key-Value Observing Programming Guide
  4. Blocks Programming Topics
  5. Transitioning to ARC Release Notes
  6. Concepts in Objective-C Programming: Delegates and Data Sources
  7. Programming with Objective-C: Working with Protocols
  8. Concepts in Objective-C Programming: Target-Action
  9. stackoverflow: How to cancel NSBlockOperation
  10. Notification Programming Topics
  11. NSKeyValueObserving Protocol Reference
  12. iOS Debugging Magic
  13. NSHipster: Key-Value Observing
  14. ReactiveCocoa
  15. MAKVONotificationCenter
  16. Weak properties KVO compliance
  17. Method Swizzling
  18. Grand Central Dispatch (GCD) Reference
  19. Simple and Reliable Threading with NSOperation

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB