OC 底层原理(13)-- KVO (上)(基本使用,自定义KVO)

366 阅读9分钟

##一、KVO 初探

添加观察者

KVO 就是观察者,观察一个值的变化,通过以下代码来理解

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person  = [LGPerson new];
    self.student = [LGStudent shareInstance];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

代码里面的使用场景,可以根据自己的理解完善场景。

以上就是一个对象添加KVO的语言,很简单,需要注意的是 context 参数不填时,要写 NULL,不要写 nil,因为context 的类型是 void *,根据上面代码可以看到有两个对象,观察的都是同一个 keyPath ,在 observeValueForKeyPath 里要怎么区分呢?

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

这时 context 就起作用了,我们可以将 context 理解为tag,我们可以为每个keyPath都设一个key,以便快速定位观察。

static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];

移除观察者

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

移除观察者和不移除对比

  • 添加移除移除动作

LGViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person  = [LGPerson new];
    self.student = [LGStudent shareInstance];
    
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];

    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name  = @"null";
    self.student.name = @"森海北语";
}

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

LGDetailViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    self.student = [LGStudent shareInstance];
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.student.name = @"hello word";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    NSLog(@"LGDetailViewController :%@",change);
}

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

打印:

从 LGViewController 可以进入 LGDetailViewController

LGViewController 界面里对name 的值做了两次改动,所以第一部分都是在 LGViewController 里面打印的,LGPerson 为 LGStudent 的父类;

然后进入 LGDetailViewController 之后对 name 添加了观察者并它做了值改变,所以第二部分在 LGDetailViewController 和LGViewController里都打印了 hello word;

第三部分是返回上一页到 LGViewController 此时就跟第一部分打印的一样,因为 LGViewController 中的 dealloc 并没有及时执行。

LGViewController -> LGDetailViewController -> LGViewController

  • 没有添加移除动作

其他代码一样,只用将 LGDetailViewController 中的移除观察的方法注释掉,然后运行时按照上面操作再来一次

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

打印结果:

然后在第三部分看到出现崩溃就是在返回LGViewController界面里,改变值是闪退了,理由是出现了野指针了

所以一定要移除观察!!!

实现观察者删除又添加需求

方法一:删除代码

方法二:可以使用这个 automaticallyNotifiesObserversForKey 自动来管理观察开关,这里以 LGPerson 为例

LGViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person  = [LGPerson new];
    self.student = [LGStudent shareInstance];
    
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];

    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name  = @"null";
    self.person.nick  = @"Nil";
    self.student.name = @"森海北语";
}

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

LGDetailViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    self.student = [LGStudent shareInstance];
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.student.name = @"hello word";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    NSLog(@"LGDetailViewController :%@",change);
}

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

LGPerson.m

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

- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

打印结果:

当 automaticallyNotifiesObserversForKey 返回NO,就是关闭了与 LGPerson 相关的观察者,LGStudent 因为是继承 LGPerson,它自己没有属性用的也是父类的,所以 LGStudent 关于nime的观察者也被关闭了,但是在 willChangeValueForKey 和 didChangeValueForKey 之间的 nick 的值改变时,还能继续观察。

模拟进度条下载-要受观察的属性受多个其他属性影响

在LGPerson 添加几个属性 downloadProgress, writtenData,totalData,在 LGViewController 中进行观察

LGViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person  = [LGPerson new];

    self.person.writtenData = 0;
    self.person.totalData = 100;
    
    [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 += 20;
}

LGPerson.m

+ (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];
}

打印结果:

初始值 writtenData = 0;totalData = 100,当执行了 self.person.writtenData += 10; 时,downloadProgress = 10/100 = 0.100000; 当执行了 self.totalData = 100;时,,downloadProgress = 10/120 = 0.083333。

观察可变数组

在 LGPerson 属性列表中添加一个可变数组 dateArray

LGViewController.m

- (void)viewDidLoad {
    self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //数组变化
    [self.person.dateArray addObject:@"1"];

}

LGPerson.m

- (void)insertObject:(id)object inDateArrayAtIndex:(NSUInteger)index{
    [self.dateArray insertObject:object atIndex:index];
}

-(void)removeObjectFromDateArrayAtIndex:(NSUInteger)index{
    [self.dateArray removeObjectAtIndex:index];
}

打印结果:

啥也没有,这是为什么呢?数组的变化 KVO 普通的键值观察不走set的,对于可变数组是由特殊的处理的,KVO 建立在 KVC基础之上,所以改变值得地方需要改一下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //数组变化
    [self.person.dateArray addObject:@"1"];
    
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];

}

打印:

KVO 原理探索分析

先给出探索代码

LGViewController.m

- (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];
}

LGPerson.m

@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

打印结果:

从打印看,执行了个 KVO 之后生成了一个 NSKVONotifying_LGPerson 动态类,但是修改的是原对象的isa

那 LGPerson 与 NSKVONotifying_LGPerson 的关系是什么呢?

继承关系

验证:

LGViewController.m 需要做一下改动

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[LGPerson alloc] init];
    
    [self printClasses:[LGPerson class]];
    
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    [self printClasses:[LGPerson class]];
    
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

#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);
}

打印结果:

我们添加了一个打印打印了 LGPerson 类及子类的方法,第一个打印是指注册 KVO之前 进行的,可以看大很正常,打印了类 LGPerson 和它的子类 LGStudent

第二个打印是在添加了一个 KVO 之后打印的,这里可以看到 LGPerson 下面有两个子类,NSKVONotifying_LGPerson 就是那个 KVO 的过程中生成的动态类。

此时我们对 "name","nickName"进行了观察,然后在 touch 事件里对他做了,值改变

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"Janice";
    self.person->name    = @"ty";
}

打印结果:

从打印结果看出,此时被观察到的,只有 nickName 的值变化,那为什么name没有呢,这就是实例变量与属性,这两者的区别就是Setter方法,实例变量没有setter,所以 name 没有被观察到,也就是说,KVO 观察的其实就是setter方法

动态子类:NSKVONotifying_XXX

打印动态子类里的方法列表

LGViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[LGPerson alloc] init];
    
    [self printClasses:[LGPerson class]];
    
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    [self printClasses:[LGPerson class]];
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LGPerson")];
    
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}

#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);
}

#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    NSLog(@"*********************");
    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);
}

打印结果:

KVO 的原理

  • 1、动态生成子类:NSKVONotifying_XXX
  • 2、观察的是 setter
  • 3、动态子类重写了很多方法 setNickName(setter)、 class、dealloc、 _isKVO
  • 4、移除观察的时候 isa 指向回来
  • 5、动态子类不会销毁

手动开始观察

代码例子

// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}

自定义 KVO

代码实现

LGViewController.m

#import "LGViewController.h"
#import "LGPerson.h"
#import "NSObject+LGKVO.h"
#import <objc/runtime.h>

@interface LGViewController ()
@property (nonatomic, strong) LGPerson *person;
@end

@implementation LGViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[LGPerson alloc] init];
    [self.person lg_addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    // [self.person lg_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"KC";
}

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

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


#pragma mark - 遍历方法-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(@"%@",NSStringFromSelector(sel));
    }
    free(methodList);
}

#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);
}
@end

LGPerson.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

+ (instancetype)shareInstance;
@end

NS_ASSUME_NONNULL_END

LGPerson.m

#import "LGPerson.h"

@implementation LGPerson
static LGPerson *_instance = nil;

+ (instancetype)shareInstance{
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        _instance = [[super allocWithZone:NULL] init] ;
    }) ;
    return _instance ;
}

- (void)setNickName:(NSString *)nickName{
    NSLog(@"来到 LGPerson 的setter方法 :%@",nickName);
    _nickName = nickName;
}
@end

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];
    // 3: isa 指向 isa_swizzling
    object_setClass(self, newClass);
    
}

#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{
    
    // 2.1 判断是否有了
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName];// LGKVONotifying_LGPerson
    Class newClass = NSClassFromString(newClassName);
    
    if (!newClass) {
         // 2.2 申请类
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        // 2.3 注册类
        objc_registerClassPair(newClass);
        // 2.4.1 添加class方法
        SEL classSEL = NSSelectorFromString(@"class");
        Method classMethod = class_getClassMethod([self class], @selector(class));
        const char *classType = method_getTypeEncoding(classMethod);
        class_addMethod(newClass, classSEL, (IMP)lg_class, classType);
    }
    // 2.4.2 添加setter方法 setNickname
    // 判断一下
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getClassMethod([self class], setterSEL);
    const char *setterType = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterType);
    
    return newClass;
}

static void lg_setter(id self,SEL _cmd,id newValue){
    NSLog(@"来了:%@",newValue);
    
    // 应该些什么? 内部 -> 相应点 newValue  oldValue option context  -> change
    // 回调给外界
    
}

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