1. KVO detail analysis

The context of use

Go to the Official Apple documentation to see the definition of context:

Context

The context pointer in the addObserver:forKeyPath:options:context: message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.

A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.

The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the balance and interestRate properties chosen this way.

Registration method addObserver: forKeyPath: options: context: in the context can pass in any data, and can receive the data in the monitoring method.

  • Context functions: tag-distinguish, which can determine the properties of the observed object more accurately. Context is used for inheritance and multiple listening. It can also be used to pass values.

    • KVOThere is only one listening callback methodobserveValueForKeyPath:ofObject:change:context:, which we can usually specify in the registration methodcontextforNULLAnd pass in the listening methodobjectandkeyPathTo determine the triggerKVOThe source of the.
    • But if there is inheritance, such as the Person class and its two subclasses Teacher and Student, the Person, Teacher, and Student instance objects all observe the balance property of the Account object. Question:
      • When balance changes, who should handle it?
      • If it’s all handled by Person, how do you determine if it’s your own transaction or a subclass’s transaction in the person class’s listener method?
    • By usingcontextYou can solve this problem nicely in the registration methodcontextSet a unique value, and then set the values in the listener methodcontextThe value can be checked.
  • Apple’s recommended usage: Use context to precisely determine the properties of the observed object, and use the address of a uniquely named static variable as the context’s value. You can set a context for the entire class, and then use object and keyPath in the listener methods to determine the properties to be viewed, so that the presence of inheritance can be determined by context; You can also set a different context for each property of the observed object, so that you can use the context to accurately determine the properties of the observed object.

    static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
    static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
    Copy the code

    - (void)registerAsObserverForAccount:(Account*)account {
        [account addObserver:self
                  forKeyPath:@"balance"
                     options:(NSKeyValueObservingOptionNew |
                              NSKeyValueObservingOptionOld)
                     context:PersonAccountBalanceContext];
    
        [account addObserver:self
                  forKeyPath:@"interestRate"
                     options:(NSKeyValueObservingOptionNew |
                              NSKeyValueObservingOptionOld)
                      context:PersonAccountInterestRateContext];
    }
    Copy the code

    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context {
    
        if (context == PersonAccountBalanceContext) {
            // Do something with the balance…
    
        } else if (context == PersonAccountInterestRateContext) {
            // Do something with the interest rate…
    
        } else {
            // Any unrecognized context must belong to super
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                   context:context];
        }
    }
    Copy the code
  • Context advantages: Less nesting, high performance, more security, and strong scalability.

  • Context note:

    • If an object is passed, a strong reference to it must be held before the observation is removed, or accessed in the listener methodcontextCould lead toCrash;
    • airborneNULLYou shouldn’tnil.

Remove observer

For the observer, many people on the Internet say that iOS9 will not be removed, is that right? If we go to apple’s official documentation, we can find the following:

An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.

  • The short answer is: whenThe observerIt doesn’t actively remove itself when it’s released.To observe the objectNotifications will continue to be sent, and messages will be sent to memory that has been freed, and exceptions will be thrownCrash. soThe observerTo remove themselves before release, use the following example to illustrate.

Create the SSLDetailViewController class:

@interface SSLDetailViewController () @property (nonatomic, strong) SSLStudent *student; @implementation SSLDetailViewController // student is the property of the previous page - (instancetype)initWithStudent:(SSLStudent *)student { if (self = [super init]) { self.student = student; } return self; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor orangeColor]; // weak observer [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];  } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.student.name = @"hello word"; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"SSLDetailViewController :%@",change); } - (void)dealloc {} @endCopy the code
  • studentThe value is passed from the property of the previous page as an object to observe.
  • SSLDetailViewControllerIs the observer,deallocThere is no operation in.
  • touchesBeganchangenameSet out,KVO.

Push to the SSLDetailViewController page, and then the ‘touchesBegan’ page displays the result:

2021-07-28 17:38:06.665226+0800 KVO [34046:504715] SSLDetailViewController :{kind = 1; new = "hello word"; }Copy the code

Pop leaves the page and pushes the SSLDetailViewController page again. The student is still passing in values from the previous page.

  • So follow apple’s official recommendation: During observer initialization (initorviewDidLoadRegister as an observer during release (deallocCall the remove method, which ensures that they are paired and is an ideal way to use them.

Automatic triggering of KVO

Can be observed in the class of the object in the rewrite + (BOOL) automaticallyNotifiesObserversForKey: (nsstrings *) key methods to control the KVO trigger automatically.

If we only allow outsiders to observe the NAME property of SSLStudent, we can do the following in the SSLStudent class. This allows the outside world to observe only the name property, and even if the outside world registers listening for other properties of the SSLStudent object, KVO is not triggered when the property changes.

/ / the return value represents allow + (BOOL) are not allowed to trigger the KVO automaticallyNotifiesObserversForKey: (nsstrings *) key {BOOL automatic = NO. if ([key isEqualToString:@"name"]) { automatic = YES; } else { automatic = [super automaticallyNotifiesObserversForKey:key]; } return automatic; }Copy the code

Manual trigger of KVO

To minimize unnecessary notification-triggering operations, or to call a listening method for a property change when multiple changes are present.

KVO can be triggered manually by manually calling willChangeValueForKey: and didChangeValueForKey: before and after assigning a value to a member variable, as shown in the following example:

+ (BOOL)automaticallyNotifiesObserversOfName
{
    return NO;
}
Copy the code
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.student willChangeValueForKey:@"age"];
    self.student->_age = 18;
    [self.student didChangeValueForKey:@"age"];
}
Copy the code
  • NSKeyValueObservingOptionPrior(trigger method before and after value change respectively, that is, there are two triggers in one change)willChangeValueForKey:anddidChangeValueForKey:Was carried out at the time.
  • If the registration methodoptionsThe incomingNSKeyValueObservingOptionPrior, then you can do this by just callingwillChangeValueForKey:To trigger the one before the changeKVOCan be used to do something about a property value before it is about to change.

One-to-many relationship

In some cases, a change in one property depends on a change in one or more other properties, that is, when other properties change, the property will also change.

For example, if we want to do KVO listening on the downloadProgress property in the Download class, the change of the property depends on the change of the writtenData and totalData properties. The observer listens for downloadProgress and should also be notified when the values of the writtenData and totalData properties change.

Override the following method to indicate that the downloadProgress property depends on writtenData and totalData:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"downloadProgress"]) { NSArray *affectingKeys = @[@"writtenData",@"totalData"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } - (NSString *)downloadProgress{ if (self.writtenData == 0) { self.writtenData = 10; } if (self.totalData == 0) { self.totalData = 100; } return [[nsstrings alloc] initWithFormat: @ "% f", 1.0 f * self writtenData/self totalData]; }Copy the code

Call code in SSLDetailViewController:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.student addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.writtenData += 10;
    self.person.totalData  += 1;
}
Copy the code

Observations of mutable arrays

KVO can listen for changes in individual properties as well as collection objects. When monitoring the change of the collection object, you need to obtain the proxy object through methods such as mutableArrayValueForKey: of KVC, and use the proxy object to operate. When the internal object of the proxy object changes, KVO’s monitoring method will be triggered. The collection object contains NSArray and NSSet. (Note: KVO is not triggered if you make operational changes directly to a collection object.)

Sample code:

- (void)viewDidLoad { [super viewDidLoad]; [self.student addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL]; } - (void)touch began :(NSSet< touch *> *)touches withEvent:(UIEvent *)event{// KVC set array [[self.student mutableArrayValueForKey:@"dateArray"] addObject:@"1"]; [[self.student mutableArrayValueForKey:@"dateArray"] removeObject:@"1"]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); }Copy the code

View the print result:

2021-07-29 14:44:31.606480+0800 001-- KVO[15464:280688] {indexes = "<_NSCachedIndexSet: 0x600000b68f20>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = ( 1 ); } 2021-07-29 14:44:31.606802+0800 001-- KVO[15464:280688] {indexes = "<_NSCachedIndexSet: 0x600000b68f20>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; }Copy the code
  • aboutkindValue:
    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1,
        NSKeyValueChangeInsertion = 2,
        NSKeyValueChangeRemoval = 3,
        NSKeyValueChangeReplacement = 4,
    };
    Copy the code

Two, KVO principle analysis

KVO implementation official documentation

Let’s take a look at the Official Apple documentation to see what it says about KVO implementation:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling

The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. 

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. 

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

  • Mentioned hereKVOThe implementation of theisa-swizzlingTechnology,isaIt points to a new class, which you’ll explore with examples.

KVO dynamically generates subclasses

Get self.person class name from object_getClassName before KVO call:

  • At this momentself.personIs the name of the classSSLPersonNo problem.

Get the self.person class name from object_getClassName after KVO:

  • At this momentself.personThe class name of theNSKVONotifying_SSLPerson, so it andSSLPersonWhat is the relationship?

The test code is created as follows, printing the subclasses of SSLPerson before the KVO call and the subclasses of SSLPerson and NSKVONotifying_SSLPerson after the KVO call

- (void)viewDidLoad { [super viewDidLoad]; self.person = [[SSLPerson alloc] init]; [self printClasses:[SSLPerson class]]; [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; [self printClasses:[SSLPerson class]]; [self printClasses:objc_getClass("NSKVONotifying_SSLPerson")]; Int count = objc_getClassList(NULL, 0); int count = objc_getClassList(NULL, 0); // Create an array containing the given object NSMutableArray *mArray = [NSMutableArray arrayWithObject: CLS]; Class* classes = (Class*)malloc(sizeof(Class)*count); objc_getClassList(classes, count); for (int i = 0; i<count; i++) { if (cls == class_getSuperclass(classes[i])) { [mArray addObject:classes[i]]; } } free(classes); NSLog(@"classes = %@", mArray); }Copy the code

View the print result:

[18966:358659] classes = (SSLPerson, SSLStudent) 2021-07-29 16:10:06.847060+0800 002-- Classes = (SSLPerson, "NSKVONotifying_SSLPerson", SSLStudent) 2021-07-29 16:10:06.849750+0800 002-- Classes = ("NSKVONotifying_SSLPerson")Copy the code
  • You can see the newly createdNSKVONotifying_SSLPersonisSSLPersonAnd has no subclasses of its own.

Next, let’s look at what happens when removeObserver is called. Breakpoint to the dealloc of the ViewController and print the result:

  1. self.persontheisaIt’s pointing backSSLPerson.
  2. NSKVONotifying_SSLPersonIt didn’t destroy it. It becameSSLStuentThe subclass.

What are the methods for dynamically generated subclasses of KVO

1. View all methods

Create code like this to print all the methods of the NSKVONotifying_SSLPerson class:

- (void)viewDidLoad { [super viewDidLoad]; self.person = [[SSLPerson alloc] init]; [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; [self printClassAllMethod:objc_getClass("NSKVONotifying_SSLPerson")]; } -ivar-property - (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

View the print result:

setNickName:-0x7fff207bf03f
class-0x7fff207bdb49
dealloc-0x7fff207bd8f7
_isKVOA-0x7fff207bd8ef
Copy the code
  • All of these ways that you can print it, all of themNSKVONotifying_SSLPersonRewrite.
  • _isKVOAIs to determine whether the current class isKVOCreated.
  • deallocOf the instance variable inisaRepoint back to the originalSSLPersonClass.

2. classMethods to rewrite

Breakpoint debugging:

  • Rewrite theclassMethod, returnsNSKVONotifying_SSLPersonThe parent classSSLPerson.

3. setterMethods to rewrite

Look at the sample code:

- (void)viewDidLoad { [super viewDidLoad]; self.person = [[SSLPerson alloc] init]; [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.person.nickName = @"SSL"; self.person->name = @"Spring"; } // KVO callback - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); }Copy the code

Click on the screen to view the print result:

2021-07-29 17:49:08.827957+0800 002-- KVO principle exploration [22666:436552] {kind = 1; new = SSL; }Copy the code
  • You can see the member variablesnameIt’s not triggered,nickNameProperty is triggered, indicating insetNickNameMethodKVORelated operations of.

In the following figure, when self.person’s ISA is referred back to SSLPerson, SSL can be printed successfully, indicating that setNickName is called for the implementation of the parent class.

3. Customize KVO

Customize basic KVO functions

Create NSObject+SSLKVO class to emulate KVO.

NSObject + SSLKVO. H:

@interface NSObject (SSLKVO)

- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
Copy the code

NSObject + SSLKVO. M:

static NSString *const kSSLKVOPrefix = @"SSLKVONotifying_"; static NSString *const kSSLKVOAssiociateKey = @"kSSLKVO_AssiociateKey"; @implementation NSObject (SSLKVO) - (void)ssl_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]; SSLKVONotifying_SSLPerson object_setClass(self, newClass); SSLKVONotifying_SSLPerson object_setClass(self, newClass); / / 4: Save observer objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), Observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{// refer to the parent Class superClass = [self class]; object_setClass(self, superClass); } # 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 current %@ ",keyPath] userInfo:nil]; }} # pragma mark - dynamic subclassing - (Class) createChildClassWithKeyPath keyPath: (nsstrings *) {nsstrings * oldClassName = NSStringFromClass([self class]); NSString *newClassName = [NSString stringWithFormat:@"%@%@",kSSLKVOPrefix,oldClassName]; Class newClass = NSClassFromString(newClassName); If (newClass) return newClass; /** * If memory does not exist, create generate * parameter 1: parent class * parameter 2: name of the new class * parameter 3: additional space created by the new class */ / 2.1: NewClass = objc_allocateClassPair([self class], newClassName.utf8String, 0); // 2.2: Register class objc_registerClassPair(newClass); Sslpersonsel classSEL = NSSelectorFromString(@"class"); Method classMethod = class_getInstanceMethod([self class], classSEL); const char *classTypes = method_getTypeEncoding(classMethod); class_addMethod(newClass, classSEL, (IMP)ssl_class, classTypes); / / 2.3.2: Add setter [SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath)); Method setterMethod = class_getInstanceMethod([self class], setterSEL); const char *setterTypes = method_getTypeEncoding(setterMethod); class_addMethod(newClass, setterSEL, (IMP) ssl_set_types, setterTypes); Add deallocSEL deallocSEL = NSSelectorFromString(@"dealloc"); Method deallocMethod = class_getInstanceMethod([self) class], deallocSEL); const char *deallocTypes = method_getTypeEncoding(deallocMethod); class_addMethod(newClass, deallocSEL, (IMP)ssl_dealloc, deallocTypes); return newClass; } static void ssl_dealloc(id self,SEL _cmd){ Class superClass = [self class]; object_setClass(self, SuperClass);} static void ssl_setter(id self,SEL _cmd,id newValue){NSLog(@" come :%@",newValue); Void (*ssl_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) ssl_msgSendSuper(&superstruct,_cmd,newValue) Let our observer call // - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context // 1: Observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey)); Message is sent to the observer SEL observerSEL = @ the selector (observeValueForKeyPath: ofObject: change: context:); nsstrings * keyPath = getterForSetter(NSStringFromSelector(_cmd)); objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL); } Class ssl_class(id self,SEL _cmd){return class_getSuperclass(object_getClass(self));} #pragma mark - Obtains the name of the set method from the get method  key ===>>> 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];} #pragma mark - Obtains the name of a getter from a set method set<Key>:===> Key static NSString *getterForSetter(NSString *setter){ if (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]; } @endCopy the code

Custom KVO progression

The above code can already achieve the basic functions of KVO, but can not listen to multiple attributes, next related optimization.

Create an SSLKVOInfo class to hold observer information, add multiple instances of SSLKVOInfo to the array, and store them in self via the associated object.

SSLKVOInfo class:

typedef NS_OPTIONS(NSUInteger, LGKeyValueObservingOptions) {

    LGKeyValueObservingOptionNew = 0x01,
    LGKeyValueObservingOptionOld = 0x02,
};

@interface SSLKVOInfo : 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


@implementation SSLKVOInfo
- (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

Optimization methods in NSObject+ SSLKvo. m

- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)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]; SSLKVONotifying_SSLPerson object_setClass(self, newClass); SSLKVONotifying_SSLPerson object_setClass(self, newClass); SSLKVOInfo *info = [[SSLKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options]; NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey)); if (! observerArr) { observerArr = [NSMutableArray arrayWithCapacity:1]; [observerArr addObject:info]; objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } - (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{ NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey)); if (observerArr.count<=0) { return; } for (SSLKVOInfo *info in observerArr) { if ([info.keyPath isEqualToString:keyPath]) { [observerArr removeObject:info];  objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); break; }} if (observerarr. count<=0) {// Call the parent Class superClass = [self Class]; object_setClass(self, superClass); }} static void ssl_setter(id self,SEL _cmd,id newValue){NSLog(@" come :%@",newValue); NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd)); id oldValue = [self valueForKey:keyPath]; void (*ssl_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) ssl_msgSendSuper(&superStruct,_cmd,newValue); / / 1: Get observer NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey)); for (SSLKVOInfo *info in observerArr) { if ([info.keyPath isEqualToString:keyPath]) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1]; / / 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]; }} / / 2: a message sent to the observer SEL observerSEL = @ the selector (ssl_observeValueForKeyPath: ofObject: change: context:); objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL); }); }}}Copy the code

Custom KVO function

Through ssl_observeValueForKeyPath: ofObject: change: context: this call is not very convenient, we introduce the functional thought, through the block to realize the monitor callback, look at the next class.

SSLKVOInfo class:

@interface SSLKVOInfo : NSObject
@property (nonatomic, weak) NSObject  *observer;
@property (nonatomic, copy) NSString    *keyPath;
@property (nonatomic, copy) SSLKVOBlock  handleBlock;
@end

@implementation SSLKVOInfo

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

NSObject + SSLKVO. H:

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

@interface NSObject (SSLKVO)

- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(SSLKVOBlock)block;
- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
Copy the code

NSObject + SSLKVO. M:

static NSString *const kSSLKVOPrefix = @"SSLKVONotifying_";
static NSString *const kSSLKVOAssiociateKey = @"kSSLKVO_AssiociateKey";

@implementation NSObject (SSLKVO)

- (void)ssl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(SSLKVOBlock)block{
    
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa的指向 : SSLKVONotifying_LGPerson
    object_setClass(self, newClass);
    // 4: 保存信息
    LGInfo *info = [[LGInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

- (void)ssl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
    if (observerArr.count<=0) {
        return;
    }
    
    for (LGInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }
    
    if (observerArr.count<=0) {
        // 指回给父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
    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:@"老铁没有当前%@的setter",keyPath] userInfo:nil];
    }
}

#pragma mark -
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kSSLKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // 防止重复创建生成新类
    if (newClass) return newClass;
    /**
     * 如果内存不存在,创建生成
     * 参数一: 父类
     * 参数二: 新类的名字
     * 参数三: 新类的开辟的额外空间
     */
    // 2.1 : 申请类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2 : 注册类
    objc_registerClassPair(newClass);
    // 2.3.1 : 添加class : class的指向是LGPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)ssl_class, classTypes);
    // 2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)ssl_setter, setterTypes);
    // 2.3.3 : 添加dealloc
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)ssl_dealloc, deallocTypes);
    
    return newClass;
}

static void ssl_dealloc(id self,SEL _cmd){
    Class superClass = [self class];
    object_setClass(self, superClass);
    }

static void ssl_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    void (*ssl_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)
    ssl_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 5: 信息数据回调
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kSSLKVOAssiociateKey));
    
    for (LGInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

Class ssl_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

#pragma mark - 从get方法获取set方法的名称 key ===>>> 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];
}

#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
    
    if (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];
}

@end
Copy the code

FBKVOController exploration

FBKVOController is a Facebook open source framework based on the system KVO implementation. Support for Objective-C and Swift. GitHub:github.com/facebook/KV… .

The advantages of FBKVOController

  • The observer is automatically removed.
  • Functional programming, you can achieve a line of code systemKVOThe three steps of.
  • implementationKVOAs with the code context where the event occurs, there is no need to pass parameters across methods.
  • increasedblockandSELCustom operation pairsNSKeyValueObservingProcessing support for callbacks.
  • eachkeyPathWill correspond to ablockorSEL, do not use if judgmentkeyPath.
  • Multiple properties of an object can be monitored at the same time, simple writing.
  • Thread safety.

Example of FBKVOController

@interface ViewController () @property (nonatomic, strong) FBKVOController *kvoCtrl; @property (nonatomic, strong) SSLPerson *person; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.person = [[SSLPerson alloc] init]; self.person.name = @"ssl"; self.person.age = 18; self.person.mArray = [NSMutableArray arrayWithObject:@"1"]; [self.kvoCtrl observe:self.person keyPath:@"age" options:0 action:@selector(ssl_observerAge)]; [self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { NSLog(@"****name:%@****",change); }]; [self.kvoCtrl observe:self.person keyPath:@"mArray" options:(NSKeyValueObservingOptionNew) block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) { NSLog(@"****mArray:%@****",change); }]; } - (void)ssl_observerAge{NSLog(@" change grade "); } - (void)touch began :(NSSet< touch *> *)touches withEvent:(UIEvent *)event{NSLog(@" touch screen "); self.person.name = [NSString stringWithFormat:@"%@+",self.person]; self.person.age ++; [[self.person mutableArrayValueForKey:@"mArray"] addObject:self.person.name]; } #pragma mark - lazy - (FBKVOController *)kvoCtrl{ if (! _kvoCtrl) { _kvoCtrl = [FBKVOController controllerWithObserver:self]; } return _kvoCtrl; } @endCopy the code

FBKVOController principle

FBKVOController uses the mediator pattern by creating an instance of FBKVOController that does all the listening, callbacks, and releases.

1. How does FBKVOController listen

To observe: keyPath: options: block: methods:

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block { NSAssert(0 ! = keyPath.length && NULL ! = block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block); if (nil == object || 0 == keyPath.length || NULL == block) { return; } // create info _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block]; // observe object with info [self _observe:object info:info]; }Copy the code

Enter _observe:info: method:

- (void)_observe:(id)object info:(_FBKVOInfo *)info { // lock pthread_mutex_lock(&_lock); NSMutableSet *infos = [_objectInfosMap objectForKey:object]; // check for info existence _FBKVOInfo *existingInfo = [infos member:info]; if (nil ! = existingInfo) { // observation info already exists; do not observe it again // unlock and return pthread_mutex_unlock(&_lock); return; } // lazilly create set of infos if (nil == infos) { infos = [NSMutableSet set]; [_objectInfosMap setObject:infos forKey:object]; } // add info and oberve [infos addObject:info]; // unlock prior to callout pthread_mutex_unlock(&_lock); [[_FBKVOSharedController sharedController] observe:object info:info]; }Copy the code
  • _objectInfosMapType:
    {
      NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
      pthread_mutex_t _lock;
    }
    Copy the code
  • _FBKVOSharedControllerThe class is a singleton that stores related information.

Enter observe:info: Observe:

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info { if (nil == info) { return; } // register info pthread_mutex_lock(&_mutex); [_infos addObject:info]; pthread_mutex_unlock(&_mutex); // add observer [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]; if (info->_state == _FBKVOInfoStateInitial) { info->_state = _FBKVOInfoStateObserving; } else if (info->_state == _FBKVOInfoStateNotObserving) { // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions, // and the observer is unregistered within the callback block. // at this time the object has been registered as an observer (in Foundation KVO), // so we can safely unobserve it. [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; }}Copy the code
  • Here,objectisself.personIt was passed on instead ofVCBecause the callback implementation here usesblockI don’t need to call back toVC.
  • [_infos addObject:info];Related information is added to the singleton.
  • [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];Here it is called againKVOThe listening operation of

2. FBKVOControllerHow is it automaticremovethe

When VC releases _kvoCtrl as a property will also be released, it will call its dealloc method, automatic remove is also implemented here.

Enter FBKVOController’s dealloc:

- (void) 'dealloc' : {[unobserveAll]; pthread_mutex_destroy(&_lock); } - (void)unobserveAll { [self _unobserveAll]; }Copy the code

Enter the _unobserveAll:

- (void)_unobserveAll { // lock pthread_mutex_lock(&_lock); NSMapTable *objectInfoMaps = [_objectInfosMap copy]; // clear table and map [_objectInfosMap removeAllObjects]; // unlock pthread_mutex_unlock(&_lock); _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController]; for (id object in objectInfoMaps) { // unobserve each registered object and infos NSSet *infos = [objectInfoMaps objectForKey:object]; [shareController unobserve:object infos:infos]; }}Copy the code
  • get_FBKVOSharedControllerSingleton, callunobserve:infos:.

Enter the unobserve: infos: :

- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos { if (0 == infos.count) { return; } // unregister info pthread_mutex_lock(&_mutex); for (_FBKVOInfo *info in infos) { [_infos removeObject:info]; } pthread_mutex_unlock(&_mutex); // remove observer for (_FBKVOInfo *info in infos) { if (info->_state == _FBKVOInfoStateObserving) { [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info]; } info->_state = _FBKVOInfoStateNotObserving; }}Copy the code
  • Remove related information:
    for (_FBKVOInfo *info in infos) {
        [_infos removeObject:info];
    }
    Copy the code
  • forremoveObserverOperation:
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    Copy the code