Welcome to the iOS Exploration series.
- IOS explores the alloc process
- IOS explores memory alignment &malloc source code
- IOS explores ISA initialization & pointing analysis
- IOS exploration class structure analysis
- IOS explores cache_T analysis
- IOS explores the nature of methods and the method finding process
- IOS explores dynamic method resolution and message forwarding mechanisms
- IOS explores the dyLD loading process briefly
- The loading process of iOS explore classes
- IOS explores the loading process of classification and class extension
- IOS explores isa interview question analysis
- IOS Explore Runtime interview question analysis
- IOS explores KVC principles and customization
- IOS explores KVO principles and customizations
- IOS explores the principle of multithreading
- IOS explores multi-threaded GCD applications
- IOS explores multithreaded GCD underlying analysis
- IOS explores NSOperation for multithreading
- IOS explores multi-threaded interview question analysis
- IOS Explore the locks in iOS
- IOS explores the full range of blocks to read
Writing in the front
From KVC (key-value coding) to KVO (key-value observation), it may be used by readers, but do you really understand it? This paper will analyze the principle of KVO in all directions
1. Preliminary study on KVO
KVO (Key-value Observing) is a set of event notification mechanism provided by Apple. This mechanism allows you to notify objects of the changes of specific properties of other objects. IOS developers can use KVO to detect and respond quickly to changes in object properties, which can be a huge help in developing highly interactive, responsive applications and bidirectional 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
KVO and NSNotificatioCenter are both implementations of iOS observer mode. The difference between them is:
- As opposed to the relationship between the observed and the observer,
KVO
It’s one to one,NSNotificatioCenter
It’s one to many KVO
It is non-intrusive to the monitored object and can be monitored without modifying its internal code
Two, KVO use and attention points
1. 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 NULL and rely entirely on the key path string to determine the source of the change notification, but this approach can cause problems by causing the object’s parent class to observe the same key path for different reasons; 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.
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 distinguish 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
context
Summary of use:
- 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
static void *PersonNameContext = &PersonNameContext;
static void *ChildNameContext = &ChildNameContext;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
[self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:ChildNameContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
if (context == PersonNameContext) {
NSLog(@ "% @", change);
} else if (context == ChildNameContext) {
NSLog(@ "% @", change); }}Copy the code
3. The need to remove notifications
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.child = [FXChild new];
self.child.name = @"Feng";
[self.child 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.child.name = [NSString stringWithFormat:@"% @ +", self.child.name];
}
Copy the code
But when FXChild is created as a singleton, pop goes back to the previous page and pushes in again and the program crashes
This is because if the observation is not removed, the singleton will still exist and the wild pointer error will be reported on re-entry
This does not happen once the observer is removed — it is necessary to remove the observer
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
4. Manually trigger key 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 be observed
automaticallyNotifiesObserversForKey
Returns NO (can be set for a property only)
+ (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
situation | The callback number |
---|---|
normal | 1 |
AutomaticallyNotifiesObserversForKey to NO | 0 |
Add willChangeValueForKey, didChangeValueForKey automaticallyNotifiesObserversForKey to NO and | 1 |
Add willChangeValueForKey, didChangeValueForKey automaticallyNotifiesObserversForKey to YES and | 2 |
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
5. Observe many-to-one key values
For example, if there is a requirement of download task, the Current download progress Process can be obtained according to the Total downloads and Current downloads. This requirement can be realized in two ways:
- Respectively to observe
Total downloads
andCurrent downloads
Two properties, evaluated when one of them changesCurrent download progress Process
- implementation
keyPathsForValuesAffectingValueForKey
Methods and observationprocess
attribute
KeyPaths = Process can receive listening callbacks whenever the Total or Current download numbers change
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"process"]) {
NSArray *affectingKeys = @[@"total".@"current"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
Copy the code
But that’s not enough — you can only listen for callbacks, but not complete Process assignment — you need to override the getter method
- (NSString *)process {
if (self.total == 0) {
return @ "0";
}
return [[NSString alloc] initWithFormat:@"%f".1.0f*self.current/self.total];
}
Copy the code
Mutable arrays
There is a variable array dataArray under FXPerson, now observe it and ask if clicking the screen prints.
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [FXPerson 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:@"Felix"];
}
Copy the code
A: no
Analysis:
KVO
Is based onKVC
While mutable array direct addition is not calledA Setter method
Variable array 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:@"Felix"];
Copy the code
Three, KVO principle — ISa-Swizzling
1. Official explanation
Key-Value Observing Programming Guide
- KVO is to use
isa-swizzling
Technically realized - As the name implies, the ISA pointer points 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 registering an observer for an object’s properties, the ISA pointer to the observed object is modified to point to the intermediate class instead of the real class. The value of the ISA pointer does not necessarily reflect the actual class of the instance
- You should never rely on isa Pointers to determine class membership. Instead, you should use the class method to determine the class of an object instance
2. 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
FXPerson
The instance object ISA points toFXPerson
- After registering the observer: the class object is
FXPerson
The instance object ISA points toNSKVONotifying_FXPerson
One conclusion can be drawn from these two figures: The FXPerson 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_FXPerson and FXPerson?
Call the print subclass method before and after registering the observer — NSKVONotifying_FXPerson is a subclass of FXPerson
3. Dynamic subclass exploration
What does a dynamic subclass observe? Let’s observe the difference between the attribute variable name and the member variable nickname
Both variables change at the same time, but only the property variable listens for callbacks — indicating that dynamic subclasses observe setter methods
② Print the methods of dynamic subclasses and observation classes via run-time API
- (void)printClassAllMethod:(Class)cls {
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
FXPerson class
The imp implementation address has not changed.NSKVONotifying_FXPerson class
Overrides the parent class inFXPerson
thedealloc
methodsNSKVONotifying_FXPerson class
Overrides the base class inNSObject
theclass
Methods and_isKVOA
methods- Rewrite the
class
Methods can refer backFXPerson class
- Rewrite the
NSKVONotifying_FXPerson class
Overrides the parent class inFXPerson
thesetName
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
setName
The address pointer is different - Every time you look at one
Attribute variables
I’ll just rewrite itsetter
Methods (self-argumentation)
③ Who does Isa point to after dealloc? — Refers to the original class
④ Will dynamic subclasses be destroyed after dealloc? –
The page is pushed in again after pop to print the FXPerson class, and the subclass NSKVONotifying_FXPerson still exists
(5) automaticallyNotifiesObserversForKey generated will affect dynamic subclass –
Dynamic subclass will according to the observed properties of automaticallyNotifiesObserversForKey Boolean value to determine whether generation
4. To summarize
automaticallyNotifiesObserversForKey
forYES
Dynamic subclasses are generated when observing properties are registeredNSKVONotifying_XXX
- Dynamic subclasses look at
setter
methods - The dynamic subclass overrides the observation property
setter
Methods,dealloc
,class
,_isKVOA
methodssetter
The key () method is used to observe key valuesdealloc
Method is used to operate on isa points at release timeclass
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
dealloc
afterisa
Pointing to the metaclassdealloc
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 NSObject+FXKVO classification, open register observer method
-(void)fx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(FXKVOBlock)block;
1. Register observers
- judge
Current value keypath
Exists /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
- Judging the properties of observation
automaticallyNotifiesObserversForKey
The Boolean value returned by the
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
- Dynamically generate subclasses, add
class
Method refers 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:@ % @ % @ "", kFXKVOPrefix, 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 points to FXPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)fx_class, classTypes);
return newClass;
}
Copy the code
- Isa redirect – makes the object’s
isa
The value points to a dynamic subclass
object_setClass(self, newClass);
Copy the code
- Save the information
Because multiple attribute values may be observed, they are stored in an array in the form of attribute values-model
typedef void(^FXKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
@interface FXKVOInfo : NSObject
@property (nonatomic.weak) NSObject *observer;
@property (nonatomic.copy) NSString *keyPath;
@property (nonatomic.copy) FXKVOBlock handleBlock;
@end
@implementation FXKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(FXKVOBlock)block {
if (self= [super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
// Save the information
FXKVOInfo *info = [[FXKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey));
if(! mArray) { mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
Copy the code
2. 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)fx_setter, setterTypes);
return newClass;
}
Copy the code
A concrete implementation of setter methods
static void fx_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 (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),}; lg_msgSendSuper(&superStruct,_cmd,newValue);// Information data callback
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey));
for (FXKVOInfo *info in mArray) {
if([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code
3. 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)fx_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 fx_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
For this reason, follow the iOS Discovery Runtime interview question analysis to exchange a wave of actions
- Remove the base class
NSObject
The dealloc implementation withfx_dealloc
Exchange methods - Isa means to go back and continue calling the real
dealloc
To release - Reason for absence
+load
Method, because it is 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)fx_dealloc, deallocTypes);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self FXMethodSwizzlingWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(fx_dealloc)];
});
return newClass;
}
- (void)fx_dealloc {
Class superClass = [self class];
object_setClass(self, superClass);
[self fx_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
This article is demo, SJKVOController and FBKVO written by J_Knight_ (I suggest taking a look at this mature custom KVO)
Recently saw a boiling point on the Nuggets – “a lot of people understand the principle, but not when it comes to actually coding”
Learning is like stepping on a pit to climb a pit, some pit seen others step on, you don’t know what is going on if you don’t try. You may be scratching your head and confused, but if you don’t solve the problem, it will always stand in the way of your growth
You have to quietly top, and then surprise everyone 🌺—————— with you