iOS底层-22:KVO原理分析及使用

概述

KVO全程KeyValueObserving,是苹果提供的一套键值观察机制,它可以在对象指定属性发生改变时接到通知。

基础使用

KVO使用分为三个步骤:
1.通过addObserver:forKeyPath:options:context:注册观察者。观察者可以接收keyPath属性变化通知。
2.在观察者中实现observeValueForKeyPath:ofObject:change:context:,以接收观察属性改变的通知消息
3.当观察者不再需要接收消息时,使用removeObserver:forKeyPath:移除观察者。在观察者在内存中释放之前,必须移除,否者会crash

注册函数

注册观察者时,需要传入一个Option参数,是一个枚举值。

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    //接收新值
    NSKeyValueObservingOptionNew = 0x01,
    //接收旧值
    NSKeyValueObservingOptionOld = 0x02,
    //一旦注册,立马会调用一次
    NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
    //在变更前后都会发送通知,而不止是变更后
    NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};

还可以传入context参数,上下文参数可以传递任意数据指针,在通知方法中返回给观察者。我们可以通过该参数,来区分发生改变的类。

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext=&PersonAccountInterestRateContext;

- (void)registerAsObserverForAccount:(Account*)account {

    [account addObserver:self  forKeyPath:@"balance" options:(NSKeyValueObservingOptionNew |  NSKeyValueObservingOptionOld) ontext:PersonAccountBalanceContext];

    [account addObserver:self forKeyPath:@"interestRate" ptions:NSKeyValueObservingOptionNew | SNSKeyValueObservingOptionOld ontext:PersonAccountInterestRateContext];
}

监听方法

观察者需要实现observeValueForKeyPath:ofObject:change:context:KVO通知会通过这个方法传递,没有实现会导致crashchange字典中存放KVO相关属性的值,根据options传入的枚举值,可以取到数据。

change中还有NSKeyValueChangeKindKey,它也是一个枚举值

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
     //观察对象的值已经更改
    NSKeyValueChangeSetting = 1,
    //观察对象是否插入
    NSKeyValueChangeInsertion = 2,
    //观察对象是否删除
    NSKeyValueChangeRemoval = 3,
    //观察对象是否替换
    NSKeyValueChangeReplacement = 4,
};

移除观察者

你可以通过向观察者对象发送removeObserver:forKeyPath:context:方法,指定观察者,路径和上下文来移除观察者。

- (void)unregisterAsObserverForAccount:(Account*)account {
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
    [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];
}

注意:

  • 如果你没有注册,就调用移除方法会导致NSRangeException,注册和移除方法应该成对存在。如果这在你的程序中不可行,请将removeObserver放入try/catch中,处理潜在的异常。
  • 移除观察者时,观察者不会自动删除自己,对已释放的对象发送观察通知会触发内存访问异常。
  • 苹果推荐我们在观察者初始化是注册(如 ininitviewDidLoad),在释放期间移除(通常是dealloc)

自动开关与手动开关

KVO属性发生改变时的调用一般是自动的,可以通过重写automaticallyNotifiesObserversForKey:手动控制。(默认返回YES

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey{
  return YES;
}

手动控制nick属性的KVO

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

注册依赖键

有时候我们不仅仅是要观察一个属性,而是多个属性。需要我们重写keyPathsForValuesAffectingValueForKey:方法。

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

数组 集合 观察

监听数组等集合时,需要用mutableArrayValueForKey触发通知

NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];

[transactions addObject:newTransaction];

KVO原理

  1. KVO只针对属性,而不监听成员变量。
    准备代码:
@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

@end
- (void)setNickName:(NSString *)nickName{
    _nickName = nickName;
}

- (void)viewDidLoad {
 [super viewDidLoad];
   
    self.person = [[LGPerson 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 *)touches withEvent:(UIEvent *)event{
    NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"Mike";
     self.person->name    = @"Tom";
}

#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"%@",change);
}

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
    [self.person removeObserver:self forKeyPath:@"nickName"];
}

打印情况:

实际情况:(null)-(null)
{
    kind = 1;
    new = Mike;
}

可以看出只是监听了属性nickName,成员变量并没有监听。

  1. 查看官方文档,KVO的实现依赖于isa-swizzling技术,当观察者注册时,被观察者的isa指针被修改,指向一个中间类而不是真正的类。

    代码验证:

    在图中位置打上断点,验证注册观察者前后的isa指向

    注册观察者之后,self.personisa指向了NSKVONotifying_LGPerson

查阅了相关资料,说NSKVONotifying_LGPersonLGPerson的子类。进一步验证:


通过打印它的内存结构,发现superClass正是LGPerson

  1. 让我们看一看NSKVONotifying_LGPerson里有什么方法
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i

调用[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];方法打印结果为:

  1. NSKVONotifying_LGPerson类重写setter方法做了什么?
    通过LLDB设置,观察nickName属性

watchpoint set variable self->_person->_nickName
不能使用点语法

self.person.nickName发生改变时,自动进入断点。

也可以直接在setter方法中打断点,查看堆栈

我们可以看到在setter方法前后,调用了NSKeyValueWillChangeNSKeyValueDidChangeBySetting

  1. 移除观察者时,isa指针是否指回原本的类
    同样,在removeObserver:forKeyPath:方法前后打印isa情况


    移除观察者后,isa指针指向原本的类。

  2. 移除观察者后,NSKVONotifying_LGPerson类是否从内存中移除。
    通过以下方法,打印LGPerson的子类,看NSKVONotifying_LGPerson是否消失。

- (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

removeObserver:forKeyPath:方法后打印:

classes = (
    LGPerson,
    LGStudent,
    "NSKVONotifying_LGPerson"
)
(lldb) 

NSKVONotifying_LGPerson依然作为LGPerson的子类存在,避免了再一次注册KVO的重新开辟,节省性能。

自定义KVO

模拟KVO实现流程,自定义一份KVO代码,实现了KVO自动销毁。主要用来学习,加深记忆。

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

@interface NSObject (LYKVO)


- (void)ly_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Handle:(KVOHandle)handle;


@end

static NSString *const kLYKVOPrefix = @"LYKVONotifying_";
static NSString *const kLYKVOAssiociateKeyHandle = @"kLYKVO_AssiociateKeyHandle";
static NSString *const kLYKVOAssiociateKeyInfo = @"kLYKVO_AssiociateKeyInfo";

@implementation NSObject (LYKVO)

- (void)ly_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Handle:(KVOHandle)handle {
    
    if (!observer || !keyPath || !options) return;
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self checkSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    
    // 3.将self 指向LGKVONotifying_Class
    object_setClass(self, newClass);
    
    
    //4.重写setter方法 class_addMethod
    SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod  = class_getInstanceMethod([self class], setterSel);
    const char * setterType = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSel, (IMP)ly_setter, setterType);
    

    //5.保存block,用于传值
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyHandle), handle, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //6.保存observer keyPath options 用于传值
    LYKVOInfo *info = [[LYKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    NSMutableArray *infoArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)kLYKVOAssiociateKeyInfo);
    if (!infoArr) {
        infoArr = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)kLYKVOAssiociateKeyInfo, infoArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [infoArr addObject:info];
    
    
}

static void ly_setter(id self,SEL _cmd,id newValue) {

    SEL getSel = NSSelectorFromString(getterForSetter(NSStringFromSelector(_cmd)));
    id oldValue = objc_msgSend(self, getSel);

    //向父类发送一个setter消息
    void (*ly_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    ly_msgSendSuper(&superStruct, _cmd, newValue);


    //处理回调
    KVOHandle handle = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyHandle));
    
    NSMutableArray *mArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyInfo));
    
    for (LYKVOInfo *info in mArr) {
        handle(info.observer,info.keyPath,oldValue,newValue);
    }
      
    
}
static Class ly_class(id self, SEL _cmd) {
    
    return class_getSuperclass(object_getClass(self));
}
static void ly_dealloc(id self, SEL _cmd) {
    
    //指针指回原本的类
    Class superClass = [self class];
    object_setClass(self, superClass);
    
    //处理数据
    NSMutableArray *mArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyInfo));
    if (mArr.count <= 0) {
        [mArr removeAllObjects];
        mArr = nil;
    }
    //执行原本的dealloc方法
    objc_msgSend(self, _cmd);
}
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    
    //2.创建中间类
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLYKVOPrefix,oldClassName];
    
    Class newClass = NSClassFromString(newClassName);
    if (newClass) return newClass;
        
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    objc_registerClassPair(newClass);
    
    //3.重写dealloc方法
    SEL deallocSel = NSSelectorFromString(@"dealloc");
    Method deallocMethod  = class_getInstanceMethod([self class], deallocSel);
    const char * deallocType = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSel, (IMP)ly_dealloc, deallocType);
    
    //4.重写class方法
    SEL classSel = NSSelectorFromString(@"class");
    Method classMethod  = class_getInstanceMethod([self class], classSel);
    const char * classType = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSel, (IMP)ly_class, classType);
    
    
    return newClass;
}
#pragma mark -验证类的setter方法是否存在
- (void)checkSetterMethodFromKeyPath:(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 -只是简单的拼接,并没有考虑周全
#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
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

@interface LYKVOInfo : NSObject

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

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

@end
@implementation LYKVOInfo

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

推荐学习:
FBKVOController
GNN源码

你可能感兴趣的:(iOS底层-22:KVO原理分析及使用)