iOS- 23.KVO

1,082 阅读9分钟

ios底层文章汇总

KVO:键值观察,是一种响应式编程

KVO使用三部曲

 使用方法addObserver:forKeyPath:options:context:向观察对象注册观察者

函数原型:

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

Options参数

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
	NSKeyValueObservingOptionNew = 0x01,    //提供更改后的值
	NSKeyValueObservingOptionOld = 0x02,    //提供更改前的值
	NSKeyValueObservingOptionInitial,   //初始值,在注册观察服务时会调用一次触发方法
        NSKeyValueObservingOptionPrior   //分别在值修改前后触发方法NSKeyValueChangeNotificationIsPriorKey 可以来判断在前调用还是在后调用
}

context: 上下文,用于区分每一次观察的细节

addObserver:forKeyPath:options:context:消息中的上下文指针包含将在相应更改通知中传递回观察者的任意数据。

可以指定NULL并完全依赖密钥路径字符串来确定更改通知的来源,但是这种方法可能会导致对象出现问题,因为其超类出于不同的原因也在观察同一密钥路径。

一种更安全、更可扩展的方法是使用上下文来确保您收到的通知是针对您的观察者而不是超类的。

类中唯一命名的静态变量的地址构成了良好的上下文。在超类或子类中以类似方式选择的上下文不太可能重叠。您可以为整个类选择单个上下文,并依赖通知消息中的键路径字符串来确定更改的内容。或者可以为每个观察到的密钥路径创建不同的上下文,这样就完全不需要进行字符串比较,从而实现更高效的通知解析。

定义上下文这种方式选择的balance和interestRate属性的示例上下文。

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;

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

在观察器内部实现observeValueForKeyPath:ofObject:change:context:以接收更改通知消息

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

change字典的取值 key:NSKeyValueChangeKey

FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

value:NSKeyValueChange

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};

当观察者不应再接收消息时,使用方法removeObserver:forKeyPath:context: 注销观察者

观察者不会自动移除,如果观察者被释放后,被观察对象继续发送通知,而不理会观察者的状态,这任何其他消息一样会触发内存访问异常。因此,必须确保观察者在从内存中消失之前将自己移除。

函数原型

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

KVO自动开关-automaticallyNotifiesObserversForKey:

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}

当automaticallyNotifiesObserversForKey返回NO时,需要手动调用

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

路径处理

下载的进度 = 已下载 / 总下载

注册观察者和监听


[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
   
    self.person.writtenData += 10;
    self.person.totalData  += 1;
   
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
   
    NSLog(@"%@",change);
}

LGPerson处理+keyPathsForValuesAffectingValueForKey:

@implementation LGPerson

// 下载进度 -- writtenData/totalData

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

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}

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

@end


数组和集合类型的观察

集合对象访问定义了三种不同的代理方法,每种方法都有一个keypath变量

  • mutableArrayValueForKey:和mutableArrayValueForKeyPath:
  • mutableSetValueForKey:和mutableSetValueForKeyPath:
  • mutableOrderedSetValueForKey:和mutableOrderedSetValueForKeyPath:
 [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
 
 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
   
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}

KVO底层原理

  • 只针对属性进行观察,即重写了属性的setter
  • 在给LGPerson对象person添加观察时注册了中间类NSKVONotifying_LGPerson继承自LGPerson,并将person的isa指向了NSKVONotifying_LGPerson image.png

#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    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);
}
  • 移除观察时,将person的isa指向LGPerson,但NSKVONotifying_LGPerson类不移除

image.png

  • NSKVONotifying_LGPerson类中有哪些东西 在LGPerson中重写description方法
- (NSString *)description {
   NSLog(@"object address : %p \n", self);
    
    IMP setNickNameIMP = class_getMethodImplementation(object_getClass(self), @selector(setNickName:));
   
    NSLog(@"object setNickName: IMP %p \n", setNickNameIMP);
    
    Class objectMethodClass = [self class];
    Class objectRuntimeClass = object_getClass(self);
    Class superClass = class_getSuperclass(objectRuntimeClass);
    NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n", objectMethodClass, objectRuntimeClass, superClass);
    
    NSLog(@"object method list \n");
    unsigned int count;
    Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
    for (NSInteger i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"method Name = %@\n", methodName);
    }
    
    return @"";
}

在添加观察前后调用description方法

self.person = [[LGPerson alloc] init];
    [self.person description];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person description];

打印结果:

object address : 0x600003f4f3a0
object setNickName: IMP 0x10ff3b2e0
objectMethodClass : LGPerson, ObjectRuntimeClass : LGPerson, superClass : NSObject
object method list
method Name = description
method Name = .cxx_destruct
method Name = nickName
method Name = setNickName:


object address : 0x600003f4f3a0
object setNickName: IMP 0x7fff2591e98b
objectMethodClass : LGPerson, ObjectRuntimeClass : NSKVONotifying_LGPerson, superClass : LGPerson
object method list
method Name = setNickName:
method Name = class
method Name = dealloc
method Name = _isKVOA

重写了setNickName: 查看NSKVONotifying_LGPerson中setNickName:的具体实现 NSKeyValueWillChange --> 消息转发给父类 --> NSKeyValueDidChangeBySetting

image.png

自定义KVO

创建NSObject的分类LGKVO

NSObject+LGKVO.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

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

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

@end

NS_ASSUME_NONNULL_END


NSObject+LGKVO.m

#import "NSObject+LGKVO.h"
#import <objc/message.h>

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

@implementation NSObject (LGKVO)

- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //  2.1 申请类
    //  2.2 注册
    //  2.3 添加方法
    // 3: isa 指向
    object_setClass(self, newClass);
    // 4: 父类 setter
    // 5: 观察者去响应
    
    

}

#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:@"%@%@",kLGKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    if (newClass) return newClass;
    // kLGKVOPrefix
    //  2.1 申请类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    //  2.2 注册,如果要添加ivar需要在objc_registerClassPair之前添加
    objc_registerClassPair(newClass);
    //  2.3 添加方法 
    
    SEL classSel =@selector(class);
    Method classMethod = class_getInstanceMethod([self class], classSel);
    const char *type = method_getTypeEncoding(classMethod); class_addMethod(newClass, classSel, (IMP)lg_class, type);
    
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    Method method = class_getInstanceMethod([self class], setterSel);
    const char *type = method_getTypeEncoding(method);
    class_addMethod(newClass, setterSel, (IMP)lg_setter, type);
    
    return newClass;
}

// 子类重写的imp 
static void lg_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
}

//重写class,添加观察后调用class方法时,返回父类
Class lg_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

自定义函数式KVO

创建NSObject的分类HTKVO,并定义保存KVO信息的对象HTKVOInfo

NSObject+HTKVO.h源码

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
//kvo 回调的block
typedef void(^HTKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

@interface NSObject (HTKVO)

- (void)ht_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HTKVOBlock)block;

- (void)ht_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

NS_ASSUME_NONNULL_END

NSObject_HTKVO.m源码

#import "NSObject+HTKVO.h"
#import <objc/message.h>

static NSString *const kHTKVOPrefix = @"HTKVONotifying_";
static NSString *const kHTKVOAssiociateKey = @"kHTKVO_AssiociateKey";

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

@implementation HTKVOInfo
//HTKVOInfo初始化
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(HTKVOBlock)block{
    if (self=[super init]) {
        _observer = observer;
        _keyPath  = keyPath;
        _handleBlock = block;
    }
    return self;
}
@end

@implementation NSObject (HTKVO)
//添加观察者
- (void)ht_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HTKVOBlock)block{
    
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3:添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)ht_setter, setterTypes);
    // 4: isa的指向 : HTKVONotifying_HTPerson
    object_setClass(self, newClass);
    // 5: 保存信息
    HTKVOInfo *info = [[HTKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHTKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHTKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

//移除观察者
- (void)ht_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHTKVOAssiociateKey));
    if (observerArr.count<=0) {
        return;
    }
    
    for (HTKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kHTKVOAssiociateKey), 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 - 创建HTKVONotifying_HTPerson子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kHTKVOPrefix,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 : 添加class : class的指向是HTPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)ht_class, classTypes);
    
    return newClass;
}

//setter方法的imp指向ht_setter
static void ht_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    void (*ht_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = [self class],
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    ht_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 5: 信息数据回调
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kHTKVOAssiociateKey));
    
    for (HTKVOInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

//重写class方法
Class ht_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

使用自定义的KVO


//添加观察者
self.person = [[HTPerson alloc] init];
    [self.person ht_addObserver:self forKeyPath:@"nickName" block:^(id  _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
        NSLog(@"%@-%@",oldValue,newValue);
    }];
    
    
//改变值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nickName = [NSString stringWithFormat:@"%@+",self.person.nickName];
}


//移除观察者
- (void)dealloc{
    [self.person ht_removeObserver:self forKeyPath:@"nickName"];
}

KVO自动销毁机制

在分类中添加ht_dealloc方法,并在添加观察时将系统的dealloc方法的imp指向自定义的ht_dealloc,这样就可以在ht_dealloc中进行isa的修改

image.png 注意点

  • 关于objc_msgSend的检查关闭:target -> Build Setting -> Enable Strict Checking of objc_msgSend Calls 设置为NO

image.png

  • class方法必须重写,其目的是为了与系统一样,对外的类保持一致

    • 如果没有重写class方法,自定的KVO在注册前后的实例对象person的class就会看到是不一致的,返回的isa更改后的类,即中间类

    • 重写后class方法后的自定义KVO,在注册观察者前后其实例对象类的显示,与系统的显示是一致的

FBKVOController.png

GNU源码下载: www.gnustep.org/resources/d…

集合set的member: 函数的使用,没有重写-hash判断是直接对比地址是否相等,如果重写了-hash则调用-isEque:来判断是否相等

hash.png