Overcoming KVO's Hidden Dangers in Objective-C
- Douglas Adams
Objective-C has existed 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 - a new version of the operating system for the revolutionary iPhone, which included the AppStore application, which allows users to purchase applications created by third-party developers.
The continued success of Objective-C was ensured not only by the popularity of iOS devices and the relative ease of sales through the AppStore, but also by Apple's significant efforts to improve both standard libraries and the language itself.
According to the TIOBE rating , Objective-C surpassed C ++ in popularity by the beginning of 2013 and took third place, second only to C and Java.
Today Objective-C includes such relatively old functions as KVC and KVO , which existed 4 years before the first iPhone, and such new features as blocks (blocks that appeared in Mac OS 10.6 and iOS 4) and automatic reference counting(ARC, available on Mac OS 10.7 and iOS 5), which makes it easy to solve problems that caused serious difficulties earlier.
KVO is a technology that allows you to immediately respond in one object (observer) to changes in the state of another object (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 - a common pattern of object-oriented programming, consisting in the fact that the object is passed a reference to an arbitrary object (called a delegate) that implements a certain protocol- a fixed set of selectors. After that, the implementation of the object "manually" sends its delegate the appropriate messages. For example, UIScrollView notifies its delegate of a change in the value of its contentOffset property by invoking 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 when the same object is a delegate of several objects of the same class, it is possible to distinguish from which one the message came from.
2. Target-action. The difference between this technique and delegation is that instead of the “delegate” implementing a specific protocol, its selector is passed along with it, which will be called when a certain event occurs. This technique is most often used by descendants of UIControl , for example, a UISwitch object can be given a target-action pair to be called when the user switches this control (UIControlEventValueChanged event). Such a solution is more convenient than delegation in the case when one “target” object should respond to the same events from different sources (for example, several UISwitch).
3. Callback block. This solution consists in the fact that the link to the object being watched 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. Moreover, the implementation of the block 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 between this approach and 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 turns out to be the only one), and the programmer needs to take care every time that blocks are implemented the block captured objects on weak links. Otherwise, it can lead to cyclical strong connections and memory leaks.
Just like in the first two techniques, it is recommended to make a link to the object that calls it one of the arguments of the block, but for a slightly different reason. Despite the fact that a block can capture this link from the context anyway, it is easy to mistakenly capture an object by a strong link, or to capture the nil by 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 entities that subscribe to alerts with this name and (optionally) an entity. Subscribing to notifications is implemented either by the principle of target-action, or using the callback block.
Unlike previous approaches, using NSNotificationCenter leads to weaker dependencies between objects and allows you to sign several objects to the same alert 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 of the specified key path of the specified other object (observable) by calling the addObserver: forKeyPath: options: context: selector on it. After that, each time the value changes, 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 for changes not only in a single attribute, but also in the values of the composite key path of the observed object, usually without any modifications to the latter.
Despite the obvious power of KVO, it is not very popular among developers, and often it is treated as at least resorting only when other solutions are not available. To try to understand (and correct) the reasons for this dislike, consider a couple of examples of the use of KVO.
Suppose we have an ETRDocument class with title and isFavorite attributes
@interface ETRDocument : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic) BOOL isFavorite;
@end
and we want to implement a tabular 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 isFavorite value can be changed not only by pressing a button, but also in some way external to the cell. This will not affect the appearance of the cell, which should be fixed. When changing isFavorite, we could manually find the cells and update them by calling updateIsFavoriteButton, but this would create unnecessary communication between the classes and break the encapsulation of our cell. Therefore, we decide to sign the cell itself for changes to the document. We could make it a delegate of the document, or send notifications when isFavorite changes, but if we use KVO instead, we will not need to make any changes to 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];
}
Launch - everything works, the cell responds to a change in isFavorite. We can even remove the call to updateIsFavoriteButton from toggleIsFavorite. However, you should close the table and change the isFavorite value of one of the documents, as the application crashes with EXC_BAD_ACCESS.
What happened? Let's try to enable NSZombieEnabled and repeat the steps. This time we get a more meaningful message when falling:
*** - [ETRDocumentCell retain]: message sent to deallocated instance 0x8bcda20
Indeed, looking at the KVO documentation we will see the following :
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.
Observation does not create strong references to either the observer, the observed object, or the context. However, the documentation is silent about what will happen when one of these objects is deleted.
The context for KVO is a regular pointer from C. Even if it points to an Objective-C object, KVO will not consider it as such: it will not send messages to it or track its lifetime. Therefore, if the context is deleted, then a “hanging” link will be passed to observeValueForKeyPath, and an attempt to pass a message through it will lead to consequences similar to those we have. However, we did not use context in our example. Moreover, it will become clear further that the context has a slightly different “true” purpose.
If the observed object is deleted, then instead of stopping the observation (after all, no values can change anymore), a warning will be displayed in the console:
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:
)
after which the application will behave in an unpredictable way, and sooner or later it will crash. However, in our case, the cell stores a strong reference to the observed object, and it cannot be deleted before the cell is deleted.
If the observer is deleted, KVO will save the “hanging” link to it (which corresponds to the unsafe_unretained modifier in ARC terminology), and will send messages on it when changes are made. This is exactly what happens in our example. It is possible that in future versions the behavior of “unsafe_unretained” will be replaced by a safer “weak”, and the “hanging” links to observers will be automatically reset.
To fix this crash, just call stopObservingIsFavorite from dealloc.
There is a way to simplify the logic of our cell. Instead of observing the key path “isFavorite” of the document, the cell can observe the key path “document.isFavorite” on itself. As a result, the cell will be notified both when the isFavorite attribute changes in the linked document, and when its link to the document changes. In this case, it is still necessary 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 observe not only isFavorite, but also the title. This will save us from overriding setDocument:, but it will run into another disadvantage 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
An old (but not very kind) analysis of cases in one method with comparing strings duplicated in two other places. This is not only ugly, but also fraught with errors, like any other “copy-paste”.
We could stop at this, hoping that nothing bad would happen, and everything would work. And now it will really work. But sooner or later, something bad can still happen, and after a couple of hours in the debugger, we will become convinced that it is better not to mess with KVO.
What could happen? Let's complicate our example a bit, and suppose that we decided to create another table for displaying our documents, but with slightly more “sophisticated” cells that will also contain the document title and the same button, but along with other changes will change the background color depending on whether the document is selected.
So that the work already done is not in vain, we decide to inherit a new cell from the old one.
And to change the background of the cell, 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. But the button stopped highlighting, the title stopped updating, and updateBackgroundColor is called too often too. Obviously, ETRAdvancedDocumentCell receives observeValueForKeyPath messages related to both its own observation and ETRDocumentCell observations. What is written in this documentation? In the commentary inside the code of one of the examples we find the following lines :
Be sure to call the superclass's implementation * if it implements it *.
NSObject does not implement the method.
Of course, we know that ETRDocumentCell implements observeValueForKeyPath, which means we need to call
[super observeValueForKeyPath: keyPath ofObject: object change: change context: context] from ETRAdvancedDocumentCell.
But everything is not limited to calling an implementation from the parent class. You should process the changes that ETRAdvancedDocumentCell itself subscribed to and pass only the other changes to the parent class. Obviously, checking the values of keyPath and object is not enough: 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 appears.
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 ETRAdvancedDocumentCellIsFavoriteContext static variable contains a pointer to a fixed area of memory containing its own address. This guarantees different values for all variables declared in this way.
Obviously, the observation should also be stopped with an indication of the context. It is curious that the corresponding method was added only in iOS 5, and before that there was only an option without the context argument. This made it impossible to correctly terminate one of the observations indistinguishable by other parameters.
But what about the ETRDocumentCell: do I need to call super from it? Does the UITableViewCell class implement the observeValueForKeyPath selector? You can resort to trial and error, try to call super, get the expected drop with the exception
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: '
An -observeValueForKeyPath: ofObject: change: context: message was received but not handled.
Key path: document.title
Observed object:
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 remove the call back. But where is the guarantee that the parent class will not start (or vice versa cease) to implement observeValueForKeyPath in the next version? Even if you yourself implement the parent class, you risk forgetting to add or remove the super call in the child classes. The most reliable solution would be to carry out an appropriate check during execution. This is not done at all by calling [super respondsToSelector: ...], which always returns YES, since our class does not override respondsToSelector :, and calling it on super is like calling self. This is done using a slightly longer expression [[ETRDocumentCell superclass] instancesRespondToSelector: ...]. But as it turns out, the documentation is fooling us, and [[NSObject class] instancesRespondToSelector: @selector (observeValueForKeyPath: ofObject: change: context: )]] returns YES, and the corresponding implementation is just the same 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 call super only for observations that are not guaranteed to be called by our code, at the risk of getting an exception, skipping something superfluous.
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 given example it follows that for the correct implementation of KVO it is necessary to do a lot of non-trivial and non-obvious actions. What should they be done in a consistent manner at all levels of inheritance, which is not in the hands of the developer, if any 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 needs to clearly monitor all active observations in the observer class to ensure that they are processed in observeValueForKeyPath and stopped at the right time (for example, when the observer is deleted). This is complicated by the diversity of the associated code in several places (determination of contexts, adding, deleting and processing of observations) and is compounded by the fact that it is impossible to check for the presence of an observation, and an attempt to stop a nonexistent observation leads to an exception:
*** Terminating app due to uncaught exception 'NSRangeException ',
reason:' Cannot remove an observer
for the key path "document.title" from
because it is not registered as an observer. '
You can often find UIViewControllers that add themselves as an observer inside an implementation of one of the methods viewDidLoad, vewDidUnload, viewWillAppear, viewDidAppear, viewWillDisappear or viewDidDisappear, and stop observing in another of these methods. However, no one guarantees strict matching of these calls, especially when using custom container view controllers, especially with shouldAutomaticallyForwardAppearanceMethods returning NO. In particular, the logic of these calls for the controllers contained in the UINavigationController stack has changed in iOS 7 with the introduction of an interactive gesture for moving backward through the navigation stack. And the reference to the object passed as an observable can 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 like this, I recall how in 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 that is not only inconvenient, but also deadly for applications using it. Such situations are not uncommon in the field of programming, and a sure way out of them is to write a more convenient and secure interface that isolates and neutralizes all the shortcomings 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 for the programmer after adding it. 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 implementation of observeValueForKeyPath: when it is not enough to distinguish between observations by object and key, you have to resort to specific contexts. But even the context does not determine the specific act of adding observation, but just a line of code in which it is performed. If the same line of code is called twice with the same observer, the observed object and key path, the consequences of these two challenges cannot be distinguished. Similarly, inheritance problems are also caused by the fact that the parent and child classes are related in the details of their implementation of KVO (which must be reliably encapsulated), since their object is the same observer from the point of view of KVO.
From these considerations it follows that for a more reliable use of KVO it is necessary to give 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. Observing exactly one keyPath of exactly one object, and clearly linking this observation to its own lifetime, this object will be reliably protected from the dangers described above. When receiving a message about a change in the observed value, the only thing he will do is to notify another object by 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 , which claims to radically shift the programming paradigm in Objective-C, and in the somewhat outdated MAKVONotificationCenter .
In addition, similar changes for the same reasons were made in NSNotificationCenter: in iOS 4, the addObserverForName: object: queue: usingBlock : method was added , which returns an object that identifies the notification subscription.
The ETRKVO interface can be simplified somewhat by looking at the behavior of the options and change arguments.
NSKeyValueObservingOptions is a bitmask that can combine the following flags:
- NSKeyValueObservingOptionNew
- NSKeyValueObservingOptionOld
- NSKeyValueObservingOptionInitial
- NSKeyValueObservingOptionPrior
The first two indicate that the old and new values of the observed attribute must be present in the change argument. This can not cause any negative consequences, except for a slight slowdown.
Specifying NSKeyValueObservingOptionInitial causes obserValueForKeyPath to be called immediately when an observation is added, which, generally speaking, is useless.
Specifying NSKeyValueObservingOptionPrior causes obserValueForKeyPath to be called not only after changing the value, but also before it. However, the new value will not be transmitted, 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:
- NSKeyValueChangeNewKey
- NSKeyValueChangeOldKey
- NSKeyValueChangeKindKey
- NSKeyValueChangeIndexesKey
- NSKeyValueChangeNotificationIsPriorKey
The first two contain the same old and new values that can be requested by the corresponding options. Values of scalar types are wrapped in NSNumber or NSValue, and instead of nil, a singleton [NSNull null] object is passed.
The following two are only needed when observing a mutable collection, which is most likely a bad idea.
The last key is only transmitted in the case of a preceding change call made with the NSKeyValueObservingOptionPrior option.
Therefore, you can only consider the keys NSKeyValueChangeNewKey and NSKeyValueChangeOldKey, and pass to the block their values in expanded form.
Therefore, 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 a property name matching the corresponding getter, it is more convenient to use the selector of this getter instead of the keyPath string. In this case, autocompletion will work, and it will be less likely 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 trick here is to use weakSelf to prevent memory leaks. If the blocks captured self by a strong link, a strong link cycle would form: ETRDocumentCell → isFavoriteKVO → block → ETRDocumentCell. However, if you are actively using blocks, capturing objects using weak links should already become a habit for you.
It is worth noting that although objects of the ETRKVO class are deleted after the cells lose links to them (they are deleted themselves), and when counting the links there are no effects like waiting for garbage collection, deletion may not occur immediately, if the link got into autorelease pool.Therefore, you should always manually call stopObservation before the ETRKVO or 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;
}
The requirements for manual cessation of observations could be relaxed if automatic reference counting could zero out weak KVO links in a compliant way, that is, so that objects observing their values were notified about this. At the moment, in iOS 7, this is impossible (unless you consider the "dirty tricks" like replacing the implementation of the dealloc method).
It should not be forgotten that the change handler is called in the same thread of execution in which the change occurs. If observing an object that can be modified from another thread is justified, and is not the result of a frivolous attitude to multithreading, then the handler code should usually be wrapped in dispatch_async. In this case, you should pay particular attention to the fact that the external block does not capture objects related to a strictly defined stream (for example, UIView, UIViewController or NSManagedObject) by strong links, since this can lead to the so-called deallocation problem .
If the handler does not fire when the observed value changes, most likely the observed attribute is not KVO compliant. How to make it such is exhaustively described in the KVO Compliance and Registering Dependent Keys documentation sections . It is worth mentioning separately that even if you do not use the standard (synthesized) property setter for the property, but define the setter yourself, this property will remain KVO compliant.
Even if you are aware of all the potential dangers of KVO, and having neutralized some of them, you should not thoughtlessly use KVO in any case. The abuse of any technology leads to a phenomenon called "[Technology Name] hell." Although the connections between objects created using KVO look very weak, getting out of control, they can really hurt. In our case, “KVO hell” can be expressed in unpredictable avalanche-like triggers of observation processors, leading to unexpected consequences and killing performance, or even in cyclic calls ending in a stack overflow.
- TIOBE Programming Community Index for November 2013
- Key-Value Coding Programming Guide
- Key-Value Observing Programming Guide
- Blocks Programming Topics
- Transitioning to ARC Release Notes
- Concepts in Objective-C Programming: Delegates and Data Sources
- Programming with Objective-C: Working with Protocols
- Concepts in Objective-C Programming: Target-Action
- stackoverflow: How to cancel NSBlockOperation
- Notification Programming Topics
- NSKeyValueObserving Protocol Reference
- iOS Debugging Magic
- NSHipster: Key-Value Observing
- ReactiveCocoa
- MAKVONotificationCenter
- Weak properties KVO compliance
- Method swizzling
- Grand Central Dispatch (GCD) Reference
- Simple and Reliable Threading with NSOperation