iOS底层探索KVO

996 阅读4分钟

KVO简介

苹果官方文档KVO介绍

KVO(Key-value observing)是一种允许对象在其他对象的指定属性发生变化时被通知的机制。

KVO细节

Options

Options会影响通知中提供的更改字典的内容,以及生成通知的方式。

  • NSKeyValueObservingOptionNew:得到改变后的值
  • NSKeyValueObservingOptionOld:得到改变前的值
  • NSKeyValueObservingOptionInitial:观察最初的值(注册处理通知的方法就立马调用)
  • NSKeyValueObservingOptionPrior:值改变之前和值改变之后各调用一次

Context

addObserver:forKeyPath:options:context:方法context都传入NULL时,在observeValueForKeyPath: ofObject: change:context:方法中处理回调时,就需要通过对objectKeyPath进行判断,如果传入唯一的标识Context,就能够直接利用Context进行区分处理。

image.png image.png

移除观察者

当前页面释放的时候需要移除观察者,否则当被观察对象是一个单例的时候,页面(观察者)释放的时候,这个单例还存在,上次注册依然有效,页面再次被注册为观察者,会被认为有两个观察者,属性发生改变,触发通知,但是第一个观察者已经被释放了,这样就会造成野指针访问。

手动/自动

图片.png 默认是自动开启KVO的,当想手动控制KVO时,除了上面要返回NO,还要对被观察属性做一下处理,比如我这里以被观察属性age举例: 图片.png

KVO应用(下载进度)

  1. 定义一个被观察属性downloadProgress,及两个其他属性(totalDatawrittenData)用于计算进度
@interface DownLoader : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic,assign) double writtenData;
@property (nonatomic,assign) double totalData;
@end

2.实现downloadProgressgetter方法

- (NSString *)downloadProgress{
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

3.配置监听totalDatawrittenData,通过计算得出downloadProgress。也就是说downloadProgress变化依赖于totalDatawrittenData

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

4.使用 图片.png

对可变数组观察

使用mutableArrayValueForKey获取数组,进行数组操作。 图片.png

KVO底层原理

KVO注册

图片.png 图片.png 也就是说,实例对象p注册KVO的时候,会生成一个子类NSKVONotifying_Person,命名方式为NSKVONotifying_<className>

KVO子类

NSKVONotifying_Person实现了什么内容呢? 图片.png 重写了以下方法:

  • setAge:

  • class:将KVO生成的子类isa指针指向当前类,验证如下: image.png

  • dealloc:销毁的的时候调用。

  • _isKVOA:一个标识,表示当前类是KVO生成的子类

这里探索一下子类方法setAge:的调用。 image.png 上图[Person setAge:]调用前还调用了这些方法:

[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] 
[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
_NSSetLongLongValueAndNotify

也就是说KVO动态生成的子类的setAge方法内部会调用上面三个方法后,再次调用父类的setAge方法。

image.png 接着在处理通知的地方打上断点,查看调用栈,又调用了:

NSKeyValueDidChange
NSKeyValueNotifyObserver
[ViewController observeValueForKeyPath:ofObject:change:context:]

最后setAge监听调用顺序如下:

1.[NSKVONotifying_Person setAge:]
2._NSSetLongLongValueAndNotify
3.[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
4.[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] 
5.[Person setAge:]
6.NSKeyValueDidChange
7.NSKeyValueNotifyObserver
8.[ViewController observeValueForKeyPath:ofObject:change:context:]

在调用NSKeyValueDidChange调用后,发送通知调用_observeValueForKeyPath:ofObject:changeKind:oldValue:newValue:indexes:context:完成通知处理。

KVO移除

图片.png 当移除KVO监听的时候,类有变成最初的了,也就是说,在注册的时候,实例对象的isa会指向当前类的子类NSKVONotifying_<className>,移除监听的时候,isa又指回以前的类。

自定义KVO

#import <Foundation/Foundation.h>
#import "MWKVOInfo.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (KVO)
- (void)mw_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MWKeyValueObservingOptions)options context:(nullable void *)context;
- (void)mw_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)mw_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+KVO.h"
#import <objc/message.h>

static NSString *const kMWKVOPrefix = @"MWKVONotifying_";
static NSString *const kMWKVOAssiociateKey = @"kMWKVO_AssiociateKey";

@implementation NSObject (KVO)
- (void)mw_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MWKeyValueObservingOptions)options context:(nullable void *)context
{
    //1.不能观察成员变量,只能观察属性,通过是否有setter方法判断
    [self judgeSetterMethodFromKeyPath:keyPath];
    //2.动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3.isa的指向刚生成的子类
    object_setClass(self, newClass);
    //4.保存观察者信息
    MWKVOInfo *info = [[MWKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
    if (!observerArr) {
        observerArr = [NSMutableArray arrayWithCapacity:1];
        [observerArr addObject:info];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
}
- (void)mw_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
    if (observerArr.count<=0) {
        return;
    }
    for (MWKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
           break;
        }
    }
    if (observerArr.count<=0) {
        // 指回给父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}
#pragma mark **-动态生成子类**
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kMWKVOPrefix,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的指向是观察对象的类
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)mw_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)mw_setter, setterTypes);
    return newClass;
}
Class mw_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
static void mw_setter(id self,SEL _cmd,id newValue){
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue       = [self valueForKey:keyPath];
    void (*mw_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)
    mw_msgSendSuper(&superStruct,_cmd,newValue);
    // 1: 拿到观察者
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
    for (MWKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // 对新旧值进行处理
                if (info.options & MWKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & MWKeyValueObservingOptionOld) {
                    [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                    }
                }
                // 2: 消息发送给观察者
                SEL observerSEL = @selector(mw_observeValueForKeyPath:ofObject:change:context:);       objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
            });
        }
    }
}
- (void)mw_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
{ 
}
#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 **- 从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