📜 ⬆️ ⬇️

Overcoming the hidden dangers of KVO in Objective C

It wouldn’t be possible to get it.
- Douglas Adams

Objective C has been around since 1983 and is the same age as C ++. However, unlike the latter, it began to gain popularity only in 2008, after the release of iOS 2.0, the new version of the operating system for the revolutionary iPhone, which included the AppStore application, which allows users to purchase third-party applications.
Objective C continued to succeed not only because of the popularity of iOS devices and the relative ease of sales through the AppStore, but also through Apple's significant efforts to improve both standard libraries and the language itself.
According to the TIOBE rating, by the beginning of 2013, Objective C overtook C ++ in popularity and finished third, second only to C and Java.

Today, Objective C includes both relatively old features like KVC and KVO , which existed 4 years before the release of the first iPhone, and such new features as blocks (blocks, introduced in Mac OS 10.6 and iOS 4) and automatic reference counting (ARC). , available in Mac OS 10.7 and iOS 5), which make it easy to solve problems that caused serious difficulties earlier.
')
KVO is a technology that allows you to immediately respond in one object (the observer) to changes in the state of another object (the observed), without introducing knowledge about the type of observer in the implementation of the observed object. In Objective C, along with KVO, there are several ways to solve this problem:

1. Delegation is a common object-oriented programming pattern, consisting in that an object is passed a link to an arbitrary object (called a delegate) that implements a specific protocol — a fixed set of selectors. After this, the implementation of the object “manually” sends messages to the delegate corresponding to the occasion. For example, the UIScrollView notifies its delegate of a change in the value of its contentOffset property by calling the scrollViewDidScroll : selector.
It is recommended that one of the parameters of all protocol selectors make a reference to the object that calls it, so that in the case when the same object is a delegate of several objects of the same class, it should be possible to distinguish from which of them the message comes.

2. Target-action . The difference between this technique and delegation is that instead of the delegate implementing a particular protocol, its selector is passed along with it, which will be called upon a specific event. This technique is most often used by successors of UIControl , for example, a UISwitch object can be set to a target-action pair for the call when the user switches this control (UIControlEventValueChanged event). Such a solution is more convenient than delegation in the case when one object “target” must respond to the same events from different sources (for example, several UISwitch).

3. Callback block. This solution consists in the fact that the reference to the observed object is transmitted not to the observer object itself, but to the block. As a rule, this block is created in the same place where it is installed. At the same time, the block implementation is capable of capturing the values ​​of local variables of the scope where it is defined, eliminating the need to add a separate method and restore the context within its implementation.
An important difference of this approach from the previous ones is that if the reference to the delegate or target is weak (weak reference), then the link to the block is strong (usually it is the only one), and the programmer needs every time to implement the blocks to ensure that the block was capturing objects by weak links. Otherwise, it can lead to cyclic strong connections and memory leaks.
Just as in the first two techniques, one of the block's arguments is recommended to make a reference to the object that calls it, but for a slightly different reason. In spite of the fact that the block can capture this link from the context anyway, it is easy to grab an object by a strong link by mistake, or to capture the nil with which this link was initialized.

4. NSNotificationCenter allows you to send alerts (NSNotification) consisting of a string name and an arbitrary object from any method of any class. Such an alert will be received by any objects that subscribe to alerts with that name and (optionally) an object. A subscription to alerts is implemented either on the basis of target-action, or using a callback block.
Unlike the previous approaches, the use of NSNotificationCenter leads to weaker dependencies between objects and allows you to easily sign several objects to the same notification without additional efforts.

5. NSKeyValueObserving is an informal protocol implemented in the NSObject class that allows you to sign an arbitrary object (observer) to change the value on the specified key path of the specified other object (the observed) by calling on it the addObserver: forKeyPath: options: context :. After that, each time the value is changed, the observer will receive the observeValueForKeyPath: ofObject: change: context: message, similar to the delegation pattern.
Thus, KVO allows you to sign an unlimited number of objects on changes not only of a single attribute, but also of values ​​along the composite key path of the observed object, as a rule, without any modifications of the latter.

Despite the obvious power of KVO, it is not very popular among developers, and is often treated as at least, resorting only when other solutions are not available. To try to understand (and correct) the reasons for such dislike, consider a couple of examples of using KVO.

Suppose we have an ETRDocument class that has the title and isFavorite attributes

@interface ETRDocument : NSObject @property (nonatomic, copy) NSString *title; @property (nonatomic) BOOL isFavorite; @end 


and we want to implement a table cell displaying information about the document

 @class ETRDocument; @interface ETRDocumentCell : UITableViewCell @property (nonatomic, strong) ETRDocument *document; @property (nonatomic, strong) IBOutlet UILabel *titleLabel; @property (nonatomic, strong) IBOutlet UIButton *isFavoriteButton; - (IBAction)toggleIsFavorite; @end @implementation ETRDocumentCell - (void)updateIsFavoriteButton { self.isFavoriteButton.selected = self.document.isFavorite; } - (void)toggleIsFavorite { self.document.isFavorite = !self.document.isFavorite; [self updateIsFavoriteButton]; } - (void)setDocument:(ETRDocument *)document { _document = document; self.titleLabel.text = self.document.title; [self updateIsFavoriteButton]; } @end 

Suppose we find that the value of isFavorite can be changed not only by pressing a button, but also in some way external to the cell. This does not affect the appearance of the cell, which should be corrected. When we changed the isFavorite, we could manually find the cells and update them by calling updateIsFavoriteButton, but this would create unnecessary connections between the classes and break the encapsulation of our cell. Therefore, we decide to sign the cell itself on the changes in the document. We could make it a document delegate, or send notifications when the isFavorite changes, but if we use KVO instead, we will not need to make any changes in the document class: all additional logic will be encapsulated in the cell class.

 - (void)startObservingIsFavorite { [self.document addObserver:self forKeyPath:@"isFavorite" options:0 context:NULL]; } - (void)stopObservingIsFavorite { [self.document removeObserver:self forKeyPath:@"isFavorite"]; } - (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]; } 

We start - everything works, the cell reacts to the change of isFavorite. We can even remove the updateIsFavoriteButton call from toggleIsFavorite. However, it is necessary to close the table and change the value of isFavorite in one of the documents, as the application falls with EXC_BAD_ACCESS.
What happened? Let's try to enable NSZombieEnabled and repeat the steps. This time we get a more sensible message when falling:
*** - [ETRDocumentCell retain]: message sent to deallocated instance 0x8bcda20

Indeed, looking into the KVO documentation we will see the following :
Note: The key-value observing: the addObserver: forKeyPath: options: context: You must ensure that you keep your references.

Observation does not create strong references to either the observer, or the observed object, or to the context. However, the documentation is silent about what happens when one of these objects is deleted.

The context for KVO is a regular pointer from the C language. Even if it points to an Objective C object, KVO will not consider it as such: will not send messages to it or track its lifetime. Therefore, if the context is removed, then the hanging link will be transmitted to observeValueForKeyPath, and an attempt to pass a message on it will lead to consequences similar to those we have. However, we did not use context in our example. Moreover, it will further become clear that the context has a slightly different “true” purpose.

If the observed object is deleted, instead of stopping the observation (after all, no values ​​can be changed anymore), a warning will be displayed in the console:

An instance 0xac62490 of class ETRDocument was a 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: 0x0, Property: 0xabf12e0>
)

after which the application will behave in an unpredictable way, and sooner or later will fall. However, in our case, the cell stores a strong link to the observed object, and it cannot be deleted before the cell is deleted.

If the observer is removed, the KVO will retain the “hanging” link to it (which corresponds to the unsafe_unretained modifier in the ARC terminology), and will send messages on it with changes. This is exactly what happens in our example. Perhaps in later versions, the behavior of “unsafe_unretained” will be replaced with a more secure “weak”, and the “hanging” links to observers will automatically be reset.
To fix this fall, just call stopObservingIsFavorite from dealloc.

There is a way to simplify the logic of our cell. Instead of observing the document's key path “isFavorite”, the cell can observe the key path “document.isFavorite” on itself. As a result, the cell will be notified both when the isFavorite attribute is changed in the associated document, and when its link to the document changes. At the same time, you still need to call removeObserver from dealloc, but you do not need to stop and start monitoring every time you change the current document.
You can go further and watch not only is Favorite, but also the title. This will save us from overriding setDocument :, but it will deal with another inconvenience of 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 

Old (but not very kind) case analysis in one method with string comparison duplicated in two other places. This is not only ugly, but fraught with errors, like any other "copy-paste".

We could stop at this, hoping that nothing bad will happen and everything will work. And now it really will work. But sooner or later, something bad may still happen, and after a couple of hours in the debugger, we will be entrenched in the conviction that it is better not to get involved with KVO.
What can happen? Let's slightly complicate our example and suppose that we decided to make another table for displaying our documents, but with slightly more “clever” cells, which will also contain the document title and the same button, but along with other changes, the background color will change depending on whether the document is a favorite.
So that the work already done is not in vain, we decide to inherit a new cell from the old one.
To change the cell background, we use the same KVO technique:

 @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]; } ... 

Great, the background changes color. That's just the button has ceased to stand out, the title has ceased to be updated, and updateBackgroundColor is called somehow too often. Obviously, ETRAdvancedDocumentCell receives observeValueForKeyPath messages, related to both own and ETRDocumentCell observations. What on this account is written in the documentation? In the comments inside the code of one of the examples we find the following lines :
Be sure to call the superclass implementation.
NSObject does not implement the method.

We, of course, know that ETRDocumentCell implements observeValueForKeyPath, which means you need to call
[super observeValueForKeyPath: keyPath ofObject: object change: change context: context] from ETRAdvancedDocumentCell.

But all is not limited to a call of implementation from a parent class. You should process the changes for which the ETRAdvancedDocumentCell itself is signed, and pass only the other changes to the parent class. Obviously, some checks of the keyPath and object values ​​are indispensable: the parent class is subscribed to exactly the same keyPath (document.isFavorite) of the same object (self). This is where the very “true” purpose of the context argument is shown.

 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]; } } ... 

The static variable ETRAdvancedDocumentCellIsFavoriteContext contains a pointer to a fixed memory region containing its own address. This guarantees different values ​​for all variables declared in this way.

It is obvious that the observation should also be stopped with an indication of the context. Curious is the fact that the corresponding method was added only in iOS 5, and before that there was only a variant without a context argument. This made it impossible to correctly stop one of the indistinguishable by other parameters of observations.

But what about ETRDocumentCell: do you need to call super from it? Does the UITableViewCell class implement the observeValueForKeyPath selector? You can resort to the trial and error method, try to call super, get the expected drop with the exception

*** 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: it 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: 0x0 '
*** 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
...

and put the call back. But where is the guarantee that the parent class will not start (or, on the contrary, stop) implement observeValueForKeyPath in the next version? Even if you implement the parent class yourself, you risk forgetting to add or remove the super call in the child classes. The most reliable solution would be to perform an appropriate check at runtime. This is not done at all by calling [super respondsToSelector: ...], which always returns YES, because our class does not override respondsToSelector :, and calling it on super doesn’t call it on self. This is done using a slightly longer expression [[ETRDocumentCell superclass] instancesRespondToSelector: ...]. But as it turns out, the documentation is deceiving us, and [[NSObject class] instancesRespondToSelector: @selector (observeValueForKeyPath: ofObject: change: context :)] returns YES, and the corresponding implementation is exactly the same and is responsible for the above exception. It turns out that we have two options: either never call super and risk breaking the logic of the parent class, or calling super only for observations not guaranteed by our code, risking getting an exception, skipping something extra.

 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 

From the above example, it follows that for the correct implementation of KVO it is necessary to do a lot of non-trivial and non-obvious actions. Moreover, they should be done in a consistent way at all levels of inheritance, which is not at the mercy of the developer, if some of these levels are implemented in standard or third-party libraries, or if the product itself is a library that assumes inheritance from some of its classes.
In addition, the programmer must clearly monitor all active observations in the observer class in order to ensure that they are processed in observeValueForKeyPath and stopped at the right time (for example, when an observer is deleted). This is complicated by the separation of the associated code in several places (defining contexts, adding, deleting and processing observations) and is aggravated by the fact that it is impossible to verify the existence of an observation, and an attempt to stop a nonexistent observation results in an exception:

*** 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. '

You can often find UIViewControllers that add themselves as observers inside the implementation of one of the viewDidLoad, vewDidUnload, viewWillAppear, viewDidAppear, viewWillDisappear or viewDidDisappear methods, and stop watching in another of these methods. However, no one guarantees the strict pairing of these calls, especially when using custom container view controllers, especially with shouldAutomaticallyForwardAppearanceMethods, which returns NO. In particular, the logic of these calls for controllers contained in the UINavigationController stack has changed in iOS 7 with the introduction of an interactive gesture to move backwards through the navigation stack. And a link to an object passed as an observable may change between these calls.
As a result, some developers even seriously suggest using solutions like the following :

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

When I see something similar, I remember how in my childhood I wrote the line “On Error Resume Next” in Visual Basic, and my “creations” miraculously stopped falling.

From all that has been written, it follows that KVO is a very powerful technology, access to which we have through an API, which is not only inconvenient, but also deadly for applications that use it. Such situations are not rare in the field of programming, and the right way out of them is to write a more convenient and safe interface that isolates and neutralizes all the flaws within its implementation.

In the case of KVO, the root of the problems with both inheritance and removeObserver is that a single observation loses its identity to the programmer after it is added. Instead of stopping “specifically this observation”, the developer is forced to demand to stop “any observation that meets the specified criteria.” Moreover, there may be several such observations, or not at all. The same thing happens in the observeValueForKeyPath implementation: when it is not enough to distinguish observations by object and key, one has to resort to specific contexts. But even the context does not define the specific act of adding observation, but only a line of code in which it is performed. If the same line of code is called twice with the same observer, the object being observed and the key path, it will not be possible to distinguish between the consequences of these two calls. Similarly, inheritance problems are also caused by the fact that the parent and child classes are related in the details of their KVO implementation (which must be securely encapsulated), since their object is the same observer from the KVO point of view.

From these considerations it follows that in order to use KVO more reliably, it is necessary to give an identity to each individual observation, namely, to create a separate object for each observation. The same object should be an observer in terms of the standard KVO interface. By observing exactly one keyPath of exactly one object, and clearly associating this observation with its own lifetime, this object will be reliably protected from the hazards described above. Receiving a message about a change in the observed value, the only thing he will do is notify another object with one of the first three methods indicated at the beginning of the article.
Let's try to implement such an object:

 @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 

Alternative solutions can be found in the ReactiveCocoa library, claiming a radical shift in the Objective-C programming paradigm, and in the somewhat outdated MAKVONotificationCenter .
In addition, similar changes were made to NSNotificationCenter for the same reasons: in iOS 4, the addObserverForName: object: queue: usingBlock : method was added that returns an object that identifies an alert subscription.

The ETRKVO interface can be somewhat simplified by considering the behavior of the options and change arguments.
NSKeyValueObservingOptions is a bitmask that can combine the following flags:


The first two indicate that the change argument should contain the old and the new values ​​of the observed attribute. It can not cause any negative consequences, except for a slight slowdown.
Specifying NSKeyValueObservingOptionInitial causes the observeValueForKeyPath to be called immediately upon adding a supervision, which, generally speaking, is useless.
Specifying NSKeyValueObservingOptionPrior causes observeValueForKeyPath to be called not only after changing the value, but also before it. However, the new value will not be transferred, even if the NSKeyValueObservingOptionNew flag is specified. The need for this can be met extremely rarely, and most likely it arises only in the process of implementing some kind of “crutch”.
Therefore, you can always pass as options (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld).

The argument (NSDictionary *) change may contain the following keys:


The first two contain the oldest and newest values ​​that can be queried with the appropriate options. Values ​​of scalar types are wrapped in NSNumber or NSValue, and instead of nil, the singleton object [NSNull null] is passed.
The following two are only needed when observing a mutable collection, which is most likely a bad idea.
The last key is transmitted only in the case of a previous change made when the NSKeyValueObservingOptionPrior option is available.
Therefore, it is possible to consider only the NSKeyValueChangeNewKey and NSKeyValueChangeOldKey keys, and transfer their values ​​to the block in expanded form.
Thus, ETRKVO can be changed as follows:

 - (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; 

Since keyPath is often the name of a property that matches the corresponding getter, it is more convenient to use the selector of this getter instead of the keyPath string. At the same time autocompletion will work, and there will be less chance to make a mistake when writing, or when renaming property.

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

Rewrite our cells using this class and category.

 @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]; } ... 

The full implementation of ETRKVO along with an example can be downloaded here.

The only non-obvious method here is to use weakSelf to prevent memory leaks. If the blocks captured self by a strong link, a cycle of strong links would form: ETRDocumentCell → isFavoriteKVO → block → ETRDocumentCell. However, if you actively use blocks, capturing objects from weak links should already become a habit.

It is worth noting that although the objects of the ETRKVO class are deleted after the cells lose references to them (they delete themselves), and when counting the links, there are no effects like waiting for garbage collection, deletion nevertheless may not happen immediately if the link is in autorelease pool.Therefore, you should always manually call stopObservation before the ETRKVO object or the observed object is deleted. When using the same property for a sequence of different observations, it is convenient to call stopObservation in its setter.

 - (void)setIsFavoriteKVO:(ETRKVO *)isFavoriteKVO { [_isFavoriteKVO stopObservation]; _isFavoriteKVO = isFavoriteKVO; } - (void)dealloc { self.isFavoriteKVO = nil; } 

Requirements for manual termination of observations could be relaxed if automatic reference counting could nullify weak KVO links in a compliant way, that is, in such a way that objects that monitor their values ​​are notified of this. At the moment, in iOS 7, this is impossible (unless we consider “dirty tricks” like the substitution of the implementation of the dealloc method).

We should not forget that the change handler is called on the same thread of execution in which this change occurs. If the observation of an object that can be changed from another thread is justified and is not a consequence of a frivolous relationship to multithreading, then the handler code should usually be wrapped in dispatch_async. At the same time, special attention should be paid to the fact that the external block does not capture objects that belong to a specific stream (for example, UIView, UIViewController or NSManagedObject) by strong references, as this can lead to the so-called deallocation problem .

If the handler fails when the observed value changes, the observed attribute is most likely not a KVO compliant. How to make it as such is fully described in the KVO Compliance and Registering Dependent Keys documentation sections . It should be separately stated that even if you do not use a standard (synthesized) property setter for a property, but define a setter yourself, this property will remain KVO compliant.

Even being aware of all the potential dangers of KVO, and neutralizing some of them, you should not uselessly use KVO in any case. Abuse of any technology leads to a phenomenon called “[Technology Name] hell”. Although the connections between objects created with the help of KVO look very weak when they spin out of control, they can hit very painfully. In our case, “KVO hell” can be expressed in unpredictable avalanche-like triggers of observation handlers leading to unexpected consequences and killing performance, or even in cyclic calls ending with a stack overflow.

  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

Source: https://habr.com/ru/post/202884/


All Articles