IOS martial arts esoteric article summary
Writing in the front
When it comes to KVC (key-value coding) and KVO (key-value observation), you probably use the yo-yo, but do you really understand it? This paper will analyze the principle of KVO in all directions
A possible secret Demo for this section
1. Preliminary study on KVO
Key-value Observing (KVO) is a set of event notification mechanism provided by Apple, which allows iOS developers to notify objects of specific property changes of other objects. IOS developers can use KVO to detect object property changes and quickly respond to them. This can help us a lot when developing strongly interactive, responsive applications and implementing bi-directional binding of views and models.
In Documentation Archieve, it is mentioned that to understand KVO, you must first understand KVC, because key-value observation is based on key-value encoding
In order to understand key-value observing, You must first understand key-value coding. — key-value Observing Programming Guide
In iOS daily development, KVO is often used to monitor the change of object properties and make timely response. That is, when the properties of the specified observed object are modified, KVO will automatically notify the corresponding observer. So what is the difference between KVO and NSNotificatioCenter?
- The same
- 1. The realization principle of both is
Observer model
, are used for listening in - 2, all can
Implement one-to-many
The operation of the
- 1. The realization principle of both is
- The difference between
- 1.
KVO
Can only be used to listen for changes in object attributes, and attribute names are passedNSString
To find, the compiler will not help you to check and complete, pure hand typing will be more prone to error - 2,
NSNotification
Send listening (post
We can control the operation ofKVO
Controlled by system - 3,
KVO
Can recordThe old and new values
change
- 1.
Two, KVO use and attention points
①. Basic use
KVO uses trilogy:
-
Registered observer
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL]; Copy the code
-
To implement the callback
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context { if ([keyPath isEqualToString:@"name"]) NSLog(@ "% @", change); } Copy the code
-
Remove observer
[self.person removeObserver:self forKeyPath:@"name"]; Copy the code
(2). The use of the context
The key-value Observing Programming Guide describes the context in this way
The context pointer in the message contains arbitrary data that is passed back to the observer in the corresponding change notification; You can
Specify the NULL
And rely entirely on the key path string to determine the source of the change notification, but this approach may cause the object’s parent class to observe the same key path for different reasons, which may cause problems; A more secure and extensible approach is to use context to ensure that the notifications you receive are addressed to the observer and not to the superclass.
The assumption here is that if the parent class has a name attribute and the subclass has a name attribute, and both register the observation of name, then it is no longer possible to tell which name has changed using keyPath alone. There are two solutions
- Add another layer of judgment — judgment
object
Obviously, it is not desirable to add logical judgment to meet business requirements - use
context
Delivering information is more secure and scalable
Generally speaking, context is mainly used to distinguish the properties of different objects with the same name. Therefore, context can be directly used in KVO callback methods to distinguish, which can greatly improve performance and code readability
Context uses summary:
-
Do not use context as an observation value
// Context is void * and should be NULL instead of nil [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL]; Copy the code
-
Use context to pass information
/ / defines the context static void *PersonNameContext = &PersonNameContext; static void *StudentNameContext = &StudentNameContext; // Register the observer [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext]; [self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:StudentNameContext]; / / KVO callback - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context { if (context == PersonNameContext) { NSLog(@ "% @", change); } else if (context == StudentNameContext) { NSLog(@ "% @", change); }}Copy the code
③ Necessity of removing notification
You may not think it matters whether or not you remove notifications in the course of daily development, but not removing notifications has potential pitfalls
Here is a piece of code that does not remove the observer, and it works fine before and after the page push and the key change
- (void)viewDidLoad {
[super viewDidLoad];
self.student = [TCJStudent new];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:ChildNameContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) NSLog(@ "% @", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.student.name = @Sunflower Treasure Book;
}
Copy the code
But when theTCJStudent
Once created as a singleton, click on the screen in push toTCJDetailViewController
Click on the screen and pop back to the previous page. Click on the screen again and the program crashes
This is because without removing the observation, the singleton still exists, and the next time you click on the screen, the wild pointer error will be reported
This does not happen when the observer is removedRemoving the observer is necessary
Apple’s official recommendation is to use addObserver for init and removeObserver for Dealloc. This ensures that add and remove are paired, which is an ideal way to use them
④ manually trigger key value observation
Sometimes a business requirement needs to observe a property value, and then it needs to be observed, and then it doesn’t… Removing and adding the entire KVO trilogy would be a tedious and unnecessary task. Fortunately, there are two ways to manually trigger key observation in KVO:
-
Will return to NO be observer automaticallyNotifiesObserversForKey (can only for a certain attribute set), automatic switch, return to the NO, we won’t be able to listen, returns YES, said listening in
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { if ([key isEqualToString:@"name"]) { return NO; } return [super automaticallyNotifiesObserversForKey:key]; } Copy the code
-
Override setter methods for observed properties with willChangeValueForKey, didChangeValueForKey
These two methods are used to inform the system that the value of the key attribute is about to change or has changed
- (void)setName:(NSString *)name { [self willChangeValueForKey:@"name"]; _name = name; [self didChangeValueForKey:@"name"]; } Copy the code
The permutations and combinations of the two methods are as follows. How to use them
Recently found [self willChangeValueForKey: name] and [self willChangeValueForKey: “name”] two kinds of writing is the result of the different: rewrite the setter method takes attribute value operation will not send additional notice; Using “name” will send an additional notification
⑤. Observe one-to-many key values
One-to-many in KVO observation, which means you can listen for changes to multiple properties by registering a SINGLE KVO observer
For example, if there is a requirement of download task, the current downloadProgress can be obtained according to total download data and current download writtenData. This requirement can be realized in two ways:
- Look at total downloads separately
totalData
And current downloadswrittenData
Two properties, one of which evaluates the current download progress when it changesdownloadProgress
- implementation
keyPathsForValuesAffectingValueForKey
Methods and observationdownloadProgress
attribute
KeyPaths =downloadProgress can be used for listening callbacks whenever the total number of downloads (totalData) or the current number of downloads (writtenData) changes
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData".@"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
Copy the code
But that’s not enough — you can only listen for the callback, but not complete the downloadProgress assignment — you need to override the getter method
- (NSString *)downloadProgress{
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f".1.0f*self.writtenData/self.totalData];
}
Copy the code
⑥.KVO looks at mutable arrays
TCJPerson: there is a variable array dataArray under TCJPerson, now observe it, ask if the screen is printed?
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [TCJPerson new];
[self.person addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"dataArray"]) NSLog(@ "% @", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person.dataArray addObject:@ "1"];
}
Copy the code
Answer: Will not analyze:
KVO
Is based onKVC
While mutable array direct addition is not calledSetter
methods- An array variable
dataArray
An error will be reported if you add it without initialization
// Initialize the mutable array
self.person.dataArray = @[].mutableCopy;
// Call setter methods
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@ "2"];
Copy the code
Three, KVO principle — ISa-Swizzling
①. Official explanation
The key-value Observing Programming Guide has a description of the underlying implementation principle
KVO
Is the use ofisa-swizzling
Technically realized- As the name suggests,
isa
Pointers point to the class that maintains the object of the allocation table, which essentially contains Pointers to the methods that the class implements, as well as other data - When an observer is registered for an object’s properties, the observed object’s
isa
Pointer to intermediate class instead of real class.isa
The value of a pointer does not necessarily reflect the actual class of the instance - You should never rely on it
isa
Pointer to determine class membership. Instead, you should useclass
Method to determine the class of an object instance
②. Code exploration
This paragraph of words in the clouds, or knock code to see the real chapter
-
Before registering an observer: the class object is
TCJPerson
, instance objectisa
Point to theTCJPerson
-
After registering the observer: the class object is
TCJPerson
, instance objectisa
Point to theNSKVONotifying_TCJPerson
One conclusion can be drawn from these two figures: the TCJPerson class does not change before and after observer registration, but the ISA direction of the instance object does
So what is the relationship between the dynamically generated middle class NSKVONotifying_TCJPerson and TCJPerson?
The print subclass method, discover, is called before and after the observer is registeredNSKVONotifying_TCJPerson
isTCJPerson
A subclass of
③ dynamic subclass exploration
You see what dynamic subclasses look at at first. Let’s look at the attribute variable nickName and the member variable name to find the difference
Both variables change at the same time, but onlyAttribute variables
Listen for callbacks – indicates that dynamic subclasses observesetter
methods
“Bluebeard’s bananas” by calling the Run-time API printed the methods of the dynamic subclass and the observation class
#pragmaMark - Traversal method -ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@ "* * * * * * * * * * * * * * * * * * * * *");
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p".NSStringFromSelector(sel),imp);
}
free(methodList);
}
Copy the code
It can be seen from printing:
TCJPerson
Method in class does not change (IMP implementation address does not change)NSKVONotifying_TCJPerson
Class overrides the parent classTCJPerson
thedealloc
methodsNSKVONotifying_TCJPerson
Class overrides the base classNSObject
theclass
Methods and_isKVOA
methods- Rewrite the
class
Methods can refer backTCJPerson
class
- Rewrite the
NSKVONotifying_TCJPerson
Class overrides the parent classTCJPerson
thesetNickName
methods- Because subclasses only inherit, do not rewrite is not a method IMP, call method will ask the parent class to method implementation
- And two
setNickName
The address pointer is different - Every time you look at one
Attribute variables
I’ll just rewrite itsetter
Methods (self-argumentation)
➌dealloc
afterisa
Point to who? — Refers to the original class
➍dealloc after dynamic subclasses are destroyed? –
The page pops and is pushed in again for printingTCJPerson
Class, subclassNSKVONotifying_TCJPerson
Class still exists
➎ automaticallyNotifiesObserversForKey will affect dynamic generate – can subclass
Dynamic subclass will according to the observed properties of automaticallyNotifiesObserversForKey Boolean value to determine whether generation
(4). To summarize
- 1.
automaticallyNotifiesObserversForKey
forYES
Dynamic subclasses are generated when observing properties are registeredNSKVONotifying_XXX
- 2. Dynamic subclasses look at
setter
methods - 3. Dynamic subclasses overwrite the observation property
setter
Methods,dealloc
,class
,_isKVOA
methodssetter
The key () method is used to observe key valuesdealloc
Method is used to release time pairsisa
Point to operateclass
Method is used to refer back to the parent of a dynamic subclass_isKVOA
A flag bit used to indicate whether it is in observer state
- 4.
dealloc
afterisa
Pointing to the metaclass - 5.
dealloc
Dynamic subclasses are not destroyed after that
4. Customize KVO
According to the official documentation of KVO and the conclusion above, we will customize KVO — the following customization will explain the use of Run-time API and interface design ideas, and the final customized KVO can meet the basic needs of use but still not perfect. The system’s KVO callbacks and auto-remove observers are layered with the registration logic, and custom KVO will use block callbacks and auto-release to optimize this
Create a new class of NSObject+TCJKVO, open register observer method
- (void)cj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(TCJKVOBlock)block;
Copy the code
①. Registered observer
1. Check whether the current observed value keypath exists or the setter method exists
The original idea is to determine whether a property exists. Although the parent class’s properties do not affect the child class, properties in the class do not have setter methods, but are added to the propertiList — eventually to determine setter methods
if (keyPath == nil || keyPath.length == 0) return;
// if (! [self isContainProperty:keyPath]) return;
if(! [self isContainSetterMethodFromKeyPath:keyPath]) return;
// Check whether the attribute exists
- (BOOL)isContainProperty:(NSString *)keyPath {
unsigned int number;
objc_property_t *propertiList = class_copyPropertyList([self class], &number);
for (unsigned int i = 0; i < number; i++) {
const char *propertyName = property_getName(propertiList[i]);
NSString *propertyString = [NSString stringWithUTF8String:propertyName];
if ([keyPath isEqualToString:propertyString]) return YES;
}
free(propertiList);
return NO;
}
/// determine the setter method
- (BOOL)isContainSetterMethodFromKeyPath:(NSString *)keyPath {
Class superClass = object_getClass(self);
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if(! setterMethod) {NSLog(@" No setter method for this property %@", keyPath);
return NO;
}
return YES;
}
Copy the code
2. The judge observed attribute automaticallyNotifiesObserversForKey method returns the Boolean value
BOOL isAutomatically = [self fx_performSelectorWithMethodName:@"automaticallyNotifiesObserversForKey:" keyPath:keyPath];
if(! isAutomatically)return;
// Call class methods dynamically
- (BOOL)fx_performSelectorWithMethodName:(NSString *)methodName keyPath:(id)keyPath {
if ([[self class] respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
BOOL i = [[self class] performSelector:NSSelectorFromString(methodName) withObject:keyPath];
return i;
#pragma clang diagnostic pop
}
return NO;
}
Copy the code
3. Dynamically generate subclasses and add a class method pointing to the original class
// Dynamically generate subclasses
Class newClass = [self createChildClassWithKeyPath:keyPath];
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@ % @ % @ "", kTCJKVOPrefix, oldClassName];
Class newClass = NSClassFromString(newClassName);
// Prevent repeated creation of new classes
if (newClass) return newClass;
/ / application class
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
/ / registered classes
objc_registerClassPair(newClass);
// Class refers to TCJPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)cj_class, classTypes);
return newClass;
}
Copy the code
4. Isa repointing — Makes the value of an object’s ISA point to a dynamic subclass
object_setClass(self, newClass);
Copy the code
5. Save information Because multiple attribute values may be observed, it is stored in an array in the form of attribute values-model
typedef void(^TCJKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
@interface TCJKVOInfo : NSObject
@property (nonatomic.weak) NSObject *observer;
@property (nonatomic.copy) NSString *keyPath;
@property (nonatomic.copy) TCJKVOBlock handleBlock;
@end
@implementation TCJKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(TCJKVOBlock)block {
if (self= [super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
// Save the information
TCJKVOInfo *info = [[TCJKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kTCJKVOAssiociateKey));
if(! mArray) { mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kTCJKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
Copy the code
②. Add setter methods and call back
Add setter methods to dynamic subclasses
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
...
/ / add a setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)cj_setter, setterTypes);
return newClass;
}
Copy the code
A concrete implementation of setter methods
static void cj_setter(id self,SEL _cmd,id newValue) {
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
// Change the value of the parent class - can be cast
void (*cj_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),}; cj_msgSendSuper(&superStruct,_cmd,newValue);// Information data callback
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kTCJKVOAssiociateKey));
for (FXKVOInfo *info in mArray) {
if([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code
③ Destroy the observer
Add the dealloc method to the dynamic subclass
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
...
/ / add dealloc
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)cj_dealloc, deallocTypes);
return newClass;
}
Copy the code
Because when the page is freed, the holding object is called dealloc when the object is freed, adding an implementation to the dynamic subclass’s dealloc method name refers isa back so that it doesn’t go to the parent class for the method implementation when the page is freed
static void cj_dealloc(id self, SEL _cmd) {
Class superClass = [self class];
object_setClass(self, superClass);
}
Copy the code
But that’s not enough, just refer isa back, but the object doesn’t call the real dealloc method, the object doesn’t get released
In this case, a wave of operations is performed based on the method exchange described in iOS ⑩: OC
- Take the dealloc implementation of the base class NSObject and swap methods with CJ_dealloc
- Isa means to go back and continue to call the real dealloc to release
- You don’t swap in the +load method because it’s inefficient, and because it affects all classes
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
...
/ / add dealloc
// SEL deallocSEL = NSSelectorFromString(@"dealloc");
// Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
// const char *deallocTypes = method_getTypeEncoding(deallocMethod);
// class_addMethod(newClass, deallocSEL, (IMP)cj_dealloc, deallocTypes);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self cj_methodSwizzlingWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(cj_dealloc)];
});
return newClass;
}
- (void)cj_dealloc {
Class superClass = [self class];
object_setClass(self, superClass);
[self cj_dealloc];
}
Copy the code
So I’m going to customize the KVO and I’m going to compose the KVO trilogy in block form in one step
Write in the back
Study harmoniously without being impatient. I’m still me, a different color of fireworks.