Last article we analyzed the principle of KVO, so can we customize our own KVO? Main ideas:
add
Observer, create an intermediate class,isa
Replacement.- The middle class
Setter implementation
Including callbacks, superclassessetter
Method calls. remove
Operation,Isa reduction
.
1. Create an intermediate class
We create a custom KVO classification based on NSObject
@interface NSObject (KBKVO)
/ / add
- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// Observe the callback- (void)kb_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
/ / remove
- (void)kb_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
Copy the code
1.1 Determination of setter methods
Our KVO is based on listening to setter methods, so we add observers first by making sure that the keypath implements the setter method.
[self judgeMethodIsExist:keyPath];// Whether the setter method is implemented- (void)judgeMethodIsExist:(NSString*)keyPath
{
Class class = object_getClass(self);
NSString *setKey = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];
SEL sel = NSSelectorFromString(setKey);
Method method = class_getInstanceMethod(class.sel);
if(! method) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"Kvo cannot be used without implementing the setter method for %@.",keyPath] userInfo:nil]; }}Copy the code
Let’s start by observing KBPerson member variable nickName, which does not implement setter method, and enter error logic
1.2 Intermediate class implementation
-(Class)createSubClassWithKeyPath:(NSString*)keyPath
{
NSString *superClassName = NSStringFromClass(self.class);
NSString *associateClassName = [NSString stringWithFormat:@"% @ % @",kKBKVOProfix,superClassName];
Class associateClass = NSClassFromString(associateClassName);
if (associateClass) {
return associateClass;//1. If it exists, return it directly without creating it
}
associateClass = objc_allocateClassPair(self.class, associateClassName.UTF8String, 0);/ / 2.1 application class
objc_registerClassPair(associateClass);/ / 2.2 registered
// 2.3.1: add a class: the class points to KBPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class].classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(associateClass, classSEL, (IMP)kb_class, classTypes);
// 2.3.2 Setter method added
NSString *setKey = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];
SEL sel = NSSelectorFromString(setKey);/ / the method name
Method method = class_getInstanceMethod(self.class, sel);
const char *type = method_getTypeEncoding(method);// Method type
class_addMethod(associateClass,sel, (IMP)kb_setter, type);// Add the method
return associateClass;
}
Class kb_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
static void kb_setter(id self,SEL _cmd,id newValue){
NSLog(@"Here comes the customization :%@",newValue);
}
Copy the code
- Let’s judge first
Does it already exist?
, does not exist to create. To apply for
Association class, and thenregistered
Association class.- Add to the associated class
setter
Method and implement it.
1.3 Intermediate classes point to instances
- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
//1. Interpret the setter method implementation
[self judgeMethodIsExist:keyPath];
//2. Implement the intermediate class
Class newClass = [self createSubClassWithKeyPath:keyPath];
//3. Isa refers to the association class
object_setClass(self, newClass);
}
Copy the code
We’re looking at the property name, which goes into the setter method of the intermediate class.
2. Setter method implementation
2.1 Normal Logic
It is understood that we will send a noticeThe system has changed
Property value, while calling the parent classsetter
Method, print as follows:When the system is called to send messages, an error will be reportedToo many arguments to function call, expected 0, have 3
Let’s modify it according to the picture.Can also beobjc_msgSendSuper
Function is a function that returns no arguments, so you need to put itForced conversion
type
void (*kb_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// objc_msgSendSuper(&superStruct,_cmd,newValue);
kb_msgSendSuper(&superStruct,_cmd,newValue);
Copy the code
After the notification observer callback, we need to send a message to the observer. So first we save the observer
NSObject *oldObserver = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));
if(! oldObserver) { objc_setAssociatedObject(self, (__bridgeconst void * _Nonnull)(kKBKVOAssociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
Copy the code
After the call, but also to record the change of value, record keypath is very troublesome to pass.
SEL observerSEL = @selector(kb_observeValueForKeyPath:ofObject:change:context:);
objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
Copy the code
2.2 Functional programming
Our daily iOS development is object-oriented development, based on MVC architecture pattern, some will use MVVM and MVP pattern to reduce the coupling. But sometimes modules have relationships that are difficult to decouple, and using functional programming can solve some of the coupling problems. For example, if f=x(), we can pass the entire function as an argument. F = x(x()). In iOS, we often use block as a parameter. I’ll have a chance to write about functional thinking and chained syntax.
- We write the notification callback directly in the add method, so it can be ours
logic
More closely,RAC framework
For suchblock
Callbacks encapsulate many system functions, such as monitoringtextField
Direct change inBlock the callback
.button
Click on theBlock the callback
And so on.
typedef void(^KBKVOBlock)(NSObject* observer,NSString*keyPath,id oldValue,id newValue); - (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KBKVOBlock)block;
Copy the code
First of all we need to save the observer, keypath, and option, so we create a class that holds that information
// Copy the system
@interface KBKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;// Prevent circular references
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) KBKVOBlock handleBlock;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handBlock:(KBKVOBlock)block;
@end
@implementation KBKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handBlock:(KBKVOBlock)block{
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
Copy the code
We put the observer, keyPath and block in the INFO when we add observers and save the info
- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KBKVOBlock)block { * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * link * * * * * * * * * * * * * * * * * * * * KBKVOInfo info = [[KBKVOInfo alloc]initWitObserver:observer forKeyPath:keyPath handBlock:block];//4.2 Save. For reusability, we put the info into an array for association
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));
if(! arr) { arr = [NSMutableArray arrayWithCapacity:1];/ / for the first time
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[arr addObject:info];
}
Copy the code
Implement the kb_setter:
static void kb_setter(id self,SEL _cmd,id newValue){
//1. Calculate the old value
NSString *keyPath = getterForSetter( NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
//1. Call the method of the superclass
Class superClass = class_getSuperclass(object_getClass(self));
struct objc_super superStruct = {
.receiver = self,
.super_class = superClass,
};
void (*kb_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// objc_msgSendSuper(&superStruct,_cmd,newValue);
kb_msgSendSuper(&superStruct,_cmd,newValue);
NSLog(@"Here comes the customization :%@",newValue);
//2. Tell the system about the change
NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));
for (KBKVOInfo *info in arr) {
if(info.handleBlock && [info.keyPath isEqualToString:keyPath]) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code
Fetch the associative array, find the info object based on the keyPath, and call the callback.
3. Remove the observer and restore ISA
Last step in the trilogy, remove. We know that removeObserver mainly does isa restore. Because the KBKVOInfo information is stored in the array of our custom associated objects, we need to remove it.
- (void)kb_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));
if (observerArr.count<=0) {
return;
}
for (KBKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break; }}if (observerArr.count<=0) {
// Return to the parent class
Class superClass = [self class]; object_setClass(self, superClass); }}/ / call- (void)dealloc
{
[self.person kb_removeObserver:self forKeyPath:@"name"];
}
Copy the code
4. Automatic removal
Since we need to manually remove the operation every time in dealloc, is there an automatic way to do it? We listen to the subclass’s dealloc method, and when dealloc is called by an observer, we call the remove operation. We’ve seen before that methd-Swizzing method swap, so we can swap dealloc subclasses.
+ (void)load
{
/ / exchange
[self kb_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(customeDealloc)]; } - (void)customeDealloc
{
//还原isa
Class superClass = [self class];
object_setClass(self, superClass);
}
// Method exchange, specific can understand methd-swizzing
+ (BOOL)kb_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
Class cls = self;
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if(! swiMethod) {return NO;
}
if(! oriMethod) { class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ })); } BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
return YES;
}
Copy the code
There are two problems with writing this way
- Our classification rewrite will result in
All the classes
It’s all switched. +load
The code inside the method doesn’t necessarilyOnly go once
.
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Execute only once to prevent swapping multiples of 2, swapping back.
[self kb_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(customeDealloc)]; }); } - (void)customeDealloc
{
Class superClass = [self class];
object_setClass(self, superClass);
[self customeDealloc];// After the imp swap, again call implementation of the original dealloc method.
}
Copy the code
Modified: 1. After the exchange of IMP, again call the implementation of the original dealloc method, does not affect the original method call. 2. To prevent active invocation, use the singleton form only to execute once, to prevent the exchange of 2 multiples, exchange back.
5. To summarize
The customization of KVO is roughly divided into three steps
- The middle class
create
(setter
Whether the method is implemented, the implementation of the intermediate class,isa
Pointing to the intermediate class) setter
Method implementation (byblock
For processing)remove
(Method exchange, automatic removal,Reduction of the isa
)
In the process, we learn the idea of functional programming, but there are some disadvantages, such as automatic removal without removing the information stored in the associative array. Git has a custom implementation of KVO. For those interested, see KVOController.