preface

“This is the 16th day of my participation in the August More Text Challenge. For details, see: August More Text Challenge.”

The customKVO

The preparatory work

In the last article, we looked at the basics of KVO in detail, so this article will follow the implementation of KVO and customize it.

Since KVO is displayed as an NSObject+NSKeyValueObserving category at the bottom, we can customize it by creating the NSObject+LGKVO category first.

In the NSObject + LGKVO. H:

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
Copy the code

In the NSObject + LGKVO. M:

First define two global static constants:

static NSString *const kLGKVOPrefix = @"LGKVONotifying_";
static NSString *const kLGKVOAssiociateKey = @"kLGKVO_AssiociateKey";
Copy the code

Then import the #import

header.

Then define the getter and setter methods separately, so that you can run debugging directly afterwards:

  • settermethods
#pragma mark - obtain the name of the set method from get ===>>> setKey: static NSString *setterForGetter(NSString *getter) { if (getter.length <= 0) { return nil; } NSString *firstString = [[getter substringToIndex:1] uppercaseString]; NSString *leaveString = [getter substringFromIndex:1]; return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString]; }Copy the code
  • gettermethods
#pragma mark - getgetterforsetter (NSString *setter){if (NSString *setter) (setter.length <= 0 || ! [setter hasPrefix:@"set"] || ! [setter hasSuffix:@":"]) { return nil; } NSRange range = NSMakeRange(3, setter.length-4); NSString *getter = [setter substringWithRange:range]; NSString *firstString = [[getter substringToIndex:1] lowercaseString]; return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString]; }Copy the code

Registered observer

Then I implement the register observer method in NSObject+ lgKvo.m,

Idea is:

  • 1: verifies whether the interface existssetterMethod: keep the instance out
  • 2: dynamically generating subclasses
  • 3: isaTo:LGKVONotifying_LGPerson
  • 4: Save the observer

The code is as follows:

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Context: (nullable void *) context {/ / 1: to verify whether there is a setter method: don't let the instance in [the self judgeSetterMethodFromKeyPath: keyPath]; / / 2: the dynamically generated Class subclass newClass = [self createChildClassWithKeyPath: keyPath]; // 3: isa point: LGKVONotifying_LGPerson object_setClass(self, newClass); // 4: save observer LGKVOInfo *info = [[LGKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options]; NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey)); if (! observerArr) { observerArr = [NSMutableArray arrayWithCapacity:1]; [observerArr addObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }}Copy the code

Verify for existencesettermethods

# pragma mark - verify whether there is a setter method - (void) judgeSetterMethodFromKeyPath keyPath: (nsstrings *) {Class superClass = object_getClass(self); SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod(superClass, setterSeletor); if (! setterMethod) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString StringWithFormat :@" old iron has no setter for the current %@ ",keyPath] userInfo:nil]; }}Copy the code

Dynamically generated subclass

Ideas:

  • Verify that a dynamic subclass has been created
    • If it is already created, return it directly
    • If it is not created, go to ②
  • (2) Create subclasses
    • The first application class, the need for the parent class, the name of the new class, open up new space
    • Registered again
    • Finally add the method, requiredclasssettermethods
# pragma mark - dynamic subclassing - (Class) createChildClassWithKeyPath keyPath: (nsstrings *) {nsstrings * oldClassName = NSStringFromClass([self class]); NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName]; Class newClass = NSClassFromString(newClassName); If (newClass) return newClass; /** * If memory does not exist, create a new class. */ // 2.1: NewClass = objc_allocateClassPair([self class], newClassName.utf8String, 0); // 2.2: register class objc_registerClassPair(newClass); // 2.3.1: add class: class refers to LGPerson SEL classSEL = NSSelectorFromString(@"class"); Method classMethod = class_getInstanceMethod([self class], classSEL); const char *classTypes = method_getTypeEncoding(classMethod); class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes); // 2.3.2: Add setsel setterSEL = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSEL); const char *setterTypes = method_getTypeEncoding(setterMethod); class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes); return newClass; }Copy the code

Save observer

To do this, create a new LGKVOInfo that inherits from NSObject to hold the observer information, add multiple instances of LGKVOInfo to the array, and then do the related operations by storing the associated objects in self. The detailed code is as follows:

In LGKVOInfo. H:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_OPTIONS(NSUInteger, LGKeyValueObservingOptions) {
    LGKeyValueObservingOptionNew = 0x01,
    LGKeyValueObservingOptionOld = 0x02,
};

@interface LGKVOInfo : NSObject

@property (nonatomic, weak) NSObject  *observer;
@property (nonatomic, copy) NSString    *keyPath;
@property (nonatomic, assign) LGKeyValueObservingOptions options;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options;

@end
NS_ASSUME_NONNULL_END
Copy the code

M: in LGKVOInfo.

#import "LGKVOInfo.h"

@implementation LGKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options{
    self = [super init];
    if (self) {
        self.observer = observer;
        self.keyPath  = keyPath;
        self.options  = options;
    }
    return self;
}
@end
Copy the code

Implement subclass methods

When subclassing dynamically, class and setter methods are overridden, and their IMPs point to lg_class and LG_setter function addresses, respectively. By storing the associated object in self, this is the same as when we analyzed KVO in the last part, and using the class method overwritten by the intermediate class, we still get the original class object, as if nothing KVO did existed.

  • lg_class
Class lg_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
Copy the code
  • lg_setter:

Ideas:

  • ① Message forwarding: forward to the parent class, change the value of the parent class
  • (2) Get the observer
  • ③, the new and old values for processing
  • (4) Send messages to observers
Static void lg_setter(id self,SEL _cmd,id newValue){NSLog(@" here :%@",newValue); NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper; // void /* struct objc_super *super, SEL op, ... */ struct objc_super superStruct = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)), }; //objc_msgSendSuper(&superStruct,_cmd,newValue) lg_msgSendSuper(&superStruct,_cmd,newValue); / / 2: Get the observer NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey)); for (LGKVOInfo *info in observerArr) { if ([info.keyPath isEqualToString:keyPath]) { dispatch_async(dispatch_get_global_queue(0, 0), ^{NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1]; / / 3 to deal with the old and new values if (info. The options & LGKeyValueObservingOptionNew) {[change setObject: newValue forKey:NSKeyValueChangeNewKey]; } if (info.options & LGKeyValueObservingOptionOld) { [change setObject:@"" forKey:NSKeyValueChangeOldKey]; if (oldValue) { [change setObject:oldValue forKey:NSKeyValueChangeOldKey]; }} / / 4: a message sent to the observer SEL observerSEL = @ the selector (lg_observeValueForKeyPath: ofObject: change: context:); objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL); }); }}}Copy the code

Remove observer

Ideas:

  • Remove observers;
  • isaRefers back to the original class object
- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{ NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey)); if (observerArr.count<=0) { return; } for (LGKVOInfo *info in observerArr) { if ([info.keyPath isEqualToString:keyPath]) { [observerArr removeObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); break; }} if (observerarr. count<=0) {Class superClass = [self Class]; object_setClass(self, superClass); }}Copy the code

At this point, the customization of KVO is complete, so let’s use:

- (void)viewDidLoad { [super viewDidLoad]; self.person = [[LGPerson alloc] init]; NSLog(@" before registering observer: %s",object_getClassName(self.person)); [self.person lg_addObserver:self forKeyPath:@"nickName" options:(LGKeyValueObservingOptionNew|LGKeyValueObservingOptionOld) context:NULL]; NSLog(@" after registering observer: %s",object_getClassName(self.person)); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.person.nickName = @"KC"; } #pragma mark -kvo callback - (void)lg_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); } - (void)dealloc{NSLog(@" remove observer before: %s",object_getClassName(self.person)); [self.person lg_removeObserver:self forKeyPath:@"nickName"]; NSLog(@"end"); NSLog(@" after removing observer: %s",object_getClassName(self.person)); }Copy the code

Take a look at the print:

The customKVO– functional

Functional programming is written as a series of nested functions and then called. Functional programming supports functions as first-class objects, which can also be called closures or functor objects.

Advantages of functional programming:

  • (1) The code is concise and the development speed is fast: functional programming uses a lot of functions to reduce the repetition of the code, so the program is short and the development speed is fast;

  • (2) Close to natural language programming, easy to understand: functional programming has a high degree of freedom, can write very close to natural language code;

  • ③, more convenient code management: functional programming does not depend on the state of the outside world, nor will it change the state of the outside world, as long as given the specified parameters, then the return result must be the same. Therefore, each function can be treated as an independent unit, which is very good for unit testing and bug debugging, and is also very easy to modular combination.

  • ④ Easy concurrent programming: functional programming does not require deadlock, because it does not modify variables, so there is no problem with locking threads. You don’t have to worry about one thread’s data being modified by another, so you can easily split your work across multiple threads and deploy concurrent programming.

So custom KVO, why use functional programming?

Through above lg_observeValueForKeyPath: ofObject: change: context: this call is not very convenient. So, let’s introduce the functional idea of using blocks to implement listening callbacks. Next, let’s optimize.

In lgkvoinfo. h, add LGKVOBlock:

@interface LGLKVOInfo : NSObject 

@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) LGKVOBlock handleBlock; 
@end 

@implementation LGLKVOInfo 
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LGKVOBlock)block { 
    if (self=[super init]) { 
        _observer = observer;
        _keyPath = keyPath; 
        _handleBlock = block; 
    } 
    return self; 
}
@end
Copy the code

In NSObject+ lgkvo. h, the lg_addObserver method also adds blocks:

typedef void(^LGKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue); 

@interface NSObject (LGKVO) 

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block; 

- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; 
@end
Copy the code

In NSObject+ lgkvo. m, the lg_addObserver method adjusts:

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LGKVOBlock)block{ // 1: Verify whether there is a setter method: don't let the instance in [the self judgeSetterMethodFromKeyPath: keyPath]; / / 2: the dynamically generated Class subclass newClass = [self createChildClassWithKeyPath: keyPath]; // 3: isa point: LGKVONotifying_LGPerson object_setClass(self, newClass); // 4: save information LGKVOInfo *info = [[LGKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block]; NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey)); if (! mArray) { mArray = [NSMutableArray arrayWithCapacity:1]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [mArray addObject:info]; }Copy the code

In NSObject+ sslkvo. m, the lg_removeObserver method adjusts:

- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{ NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey)); if (observerArr.count<=0) { return; } for (LGKVOInfo *info in observerArr) { if ([info.keyPath isEqualToString:keyPath]) { [observerArr removeObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); break; }} if (observerarr. count<=0) {Class superClass = [self Class]; object_setClass(self, superClass); }}Copy the code

In NSObject+ lgKvo. m, the lg_setter method is adjusted, instead of calling the KVO callback method, instead of calling the block saved in info:

Static void lg_setter(id self,SEL _cmd,id newValue){NSLog(@" here :%@",newValue); NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; Void (*lg_msgSendSuper)(void *,SEL, id) = (void *)objc_msgSendSuper; // void /* struct objc_super *super, SEL op, ... */ struct objc_super superStruct = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)), }; //objc_msgSendSuper(&superStruct,_cmd,newValue) lg_msgSendSuper(&superStruct,_cmd,newValue); NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey)); for (LGKVOInfo *info in mArray) { if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) { info.handleBlock(info.observer, keyPath, oldValue, newValue); }}}Copy the code

After optimization, and then in the controller inside the call, will be much simpler, the code is as follows:

- (void)viewDidLoad { [super viewDidLoad]; self.person = [[LGPerson alloc] init]; NSLog(@" before registering observer: %s",object_getClassName(self.person)); [self.person lg_addObserver:self forKeyPath:@"nickName" block:^(id _Nonnull observer, NSString * _Nonnull keyPath, Id _Nonnull oldValue, id _Nonnull newValue) {NSLog(@" callback method: %@ - %@",oldValue,newValue);}]; NSLog(@" after registering observer: %s",object_getClassName(self.person)); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.person.nickName = @"KC"; } - (void)dealloc {NSLog(@" remove observer before: %s",object_getClassName(self.person)); [self.person lg_removeObserver:self forKeyPath:@"nickName"]; NSLog(@" after removing observer: %s",object_getClassName(self.person)); }Copy the code

Print results after operation:

Self-destruct mechanism

In the previous article, we looked at whether we could override the subclass dealloc method to automatically remove the observer if it needed to be destroyed, since we had to manually remove the observer after using KVO.

  • Dynamically generated subclass: first of all increateChildClassWithKeyPathMethod to adddealloc:
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{ NSString *oldClassName = NSStringFromClass([self class]); NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName]; Class newClass = NSClassFromString(newClassName); If (newClass) return newClass; /** * If memory does not exist, create a new class. */ // 2.1: NewClass = objc_allocateClassPair([self class], newClassName.utf8String, 0); // 2.2: register class objc_registerClassPair(newClass); // 2.3.1: add class: class refers to LGPerson SEL classSEL = NSSelectorFromString(@"class"); Method classMethod = class_getInstanceMethod([self class], classSEL); const char *classTypes = method_getTypeEncoding(classMethod); class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes); // 2.3.2: Add setsel setterSEL = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSEL); const char *setterTypes = method_getTypeEncoding(setterMethod); class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes); // 2.3.3: add deallocSEL deallocSEL = NSSelectorFromString(@"dealloc"); Method deallocMethod = class_getInstanceMethod([self class], deallocSEL); const char *deallocTypes = method_getTypeEncoding(deallocMethod); class_addMethod(newClass, deallocSEL, (IMP)lg_dealloc, deallocTypes); return newClass; }Copy the code
  • Added when subclassing dynamicallydeallocMethod,IMPPoint to thelg_deallocFunction address, inlg_deallocInside the method:
Static void lg_dealloc(id self,SEL _cmd) {NSLog(@" before autodestruct: %s",object_getClassName(self)); Class superClass = [self class]; object_setClass(self, superClass); NSLog(@" after autodestruct: %s",object_getClassName(self)); }Copy the code
  • The use of the autodestruct mechanism is called directly from within the controllerdeallocMethods:
- (void)dealloc{NSLog(@" destroyed "); }Copy the code

Run the print result:

FBKVOControllerexplore

FBKVOControllerBasic use of

FBKVOController is a Facebook open source framework based on system KVO implementation. Supports Objective-C and Swift languages. Download it on gitHub

Advantages of FBKVOController:

  • No need to manually remove observers; The frame automatically removes observers for us
  • Functional programming, can be a line of code to achieve the systemKVOThree steps.
  • implementationKVOSame code context as where the event occurs, no need to pass parameters across methods;
  • increasedblockandSELCustom operation pairNSKeyValueObservingCallback processing support for improved usageKVOThe experience;
  • eachkeyPathWould correspond to ablockorSELYou don’t have toblockThe use ofifjudgekeyPathIn other words, there is no need for unifiedobserveValueForKeyPathMethods in writingifJudgment;
  • You can listen on multiple attributes of an object at the same time.
  • Thread safe.

Let’s first look at the use of FBKVOController. The code looks like this:

#import "ViewController.h" #import <FBKVOController.h> #import "LGPerson.h" @interface ViewController () @property (nonatomic, strong) FBKVOController *kvoCtrl; @property (nonatomic, strong) LGPerson *person; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.person = [[LGPerson alloc] init]; [self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { NSLog(@"****%@****",change); }]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.person.name = @"cooci"; } - (FBKVOController *)kvoCtrl{ if (! _kvoCtrl) { _kvoCtrl = [FBKVOController controllerWithObserver:self]; } return _kvoCtrl; } @endCopy the code
  • Here is the one usedblockThe callback:
[self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { 
        NSLog(@"****%@****",change); 
}]; 
Copy the code

In addition to the block call, there are other ways to implement it:

  • useactionThe callback:
[self.kvoCtrl observe:self.person keyPath:@"age" options:NSKeyValueObservingOptionNew action:@selector(lg_observerAge)];
Copy the code
  • Listening on mutable arrays
[self.kvoCtrl observe:self.person keyPath:@"mArray" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { 
    NSLog(@"****%@****",change); 
}];
Copy the code

FBKVOControllerThe source code parsing

Initialize the

Registered observer

  • in_FBKVOInfoClass, which stores observer information, including:FBKVOController,keyPath,options,blockAnd so on;
  • willobserveandinfoCorrelate;

observeandinfoCorrelation details of

  • inFBKVOControllerWhen it initializes, it holdsNSMapTable, the use ofNSMapTableReplace associated objects;
  • Then put theobjectAs akeyTo obtainNSMutableSetThe type ofvalueAnd at the same time,NSMutableSetStored in the_FBKVOInfo;
  • ifinfosEmpty, create, and add toNSMapTable;
  • wheninfosNot null after willinfoObject toNSMutableSetThe type ofinfos;
  • middle-borns_FBKVOSharedControllerIs the singleton pattern responsible for the realKVOOperation;

middle-borns_FBKVOSharedControllerAnalysis of the

  • Registration systemKVO
    • The incomingobjectCall the system for the instance objectKVOTo start listening for properties;
    • The incomingaddObservermethodsselfIs the mediator of the current singleton;
    • Intermediate holdingNSHashTableIs stored as a weak reference_FBKVOInfo;
    • through_FBKVOInfothe_stateMember variable, according to the enumerated value_FBKVOInfoStateInitial,_FBKVOInfoStateObserving,_FBKVOInfoStateNotObservingDecide to add or deleteKVO;

  • To implement the callback
    • throughinfoTo obtainFBKVOController;
    • fromFBKVOController, getobserverWhich is the real thingVC;
    • throughinfoTo obtainblockAnd calls, passing in the necessary arguments;

  • The destruction

Copy its own NSMapTable to the temporary table, clean it up, traverse the temporary table again, and remove the association between the instance object and infOS from the singleton mediator

  • Remove the systemKVOListening in
    • Call systemKVOtheremoveObserverMethod to remove the observer
    • Destruction process:VCDestruction — — >FBKVOControllerDestroy –> intermediateunobserveMethod –> Remove observer