15、iOS底层分析 - KVO


KVC、KVO是iOS中经常会用到也是面试中经常会被问到的。今天就来探究一下KVO。

什么是KVO?

KVO:键值观察机制,它提供了观察某一属性变化的方法
KVO的全称是 KeyValueObserving ,是苹果提供的一套事件通知机制。允许对象监听另一个对象属性的变化,并在改变时接收到事件。由于KVO的实现机制,所以对 属性 才会发生作用,一般继承自NSObject 的对象都默认支持KVO。
KVO 和 NSNotificationCenter 都是iOS中 观察者模式 的一种实现。

区别在于,相对被观察者和观察者之间的关系:

  • KVO是一对一的
  • NSNotificationCenter 是一对多的

KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC 的 mutableArrayValueForKey: 等方法获得代理对象,当代理对象的内部发生变化时,会回调KVO监听的方法。集合对象包含 NSArray 和 NSSet。

一、KVO的基本用法

使用KVO分为三步(KVO三部曲)

  1. 注册观察者
  2. 实现回调
  3. 移除观察者
#import "LJLKVOViewController.h"
#import <objc/runtime.h>
#import "LJLKVOPerson.h"
#import "LJLKVODownloader.h"

static void * personNameContext = &personNameContext;//观察person.name 的 context
static void * personKvoArrayContext = &personKvoArrayContext;//观察person.kvoArray 的 context

@interface LJLKVOViewController ()
@property(nonatomic, strong)LJLKVOPerson * person;
@end

1.1、注册观察者
    被观察的对象调用下面的方法来注册观察者:

self.person = [[LJLKVOPerson alloc] init];
//注册观察者
[self.person addObserver:self
              forKeyPath:@"name"
                 options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                context:personNameContext];

context 作用:快速定位观察键。
如果观察两个同名的 keyPath 的时候,不容易区分,就需要用context 来进行区分。
而且通过这样的形式,可以直接知道观察的是哪个类的哪个属性,这样做减少了在回调中嵌套多层判断,更安全也更高效。功能类似于是 tag。
如果不需要可以填NULL ,因为这个地方是nullable void * 所以填 NULL,如果是id 填nil。填nil也可以,因为xcode会帮我们把 nil 转成 NULL 编译的时候。

1.2、实现回调
注册后,观察者需要实现下面回调来接收通知:

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                      context:(void *)context
{
//这里就可以通过注册的时候的context来快速判断出观察的哪个类的哪个属性变化触发的回调
    if (context == personNameContext) {
        NSLog(@"name change : %@",change);
    }else{
        NSLog(@"change - %@",change);
    }

//change - {
//        kind = 1; 这个就是观察的类型,1是set
//        new = "0.100000";
/*
     观察类型枚举 (被观察者变更的类型)
     NSKeyValueChangeSetting = 1,       设置 例如观察的是NSString 修改的时候就是这个类型
     NSKeyValueChangeInsertion = 2,     插入 例如观察的是NSMutableArray 添加数据的时候
     NSKeyValueChangeRemoval = 3,       删除
     NSKeyValueChangeReplacement = 4,   替换
*/
}

1.3、移除观察者
    如果不再继续观察,一定要移除观察者,否则可能出现异常。养成良好习惯,以免出现一些隐藏的崩溃。
    而且如果不移除的话,再次进来又重新注册。这时候会出现多次调用的情况。如果被观察者使用的是单例,不移除观察对象就还存在就不会释放,会出现野指针,就会崩溃。

[self.student removeObserver:self forKeyPath:@"name"];

较完整的三步操作如下,当然这个被观察者不必非得写成单例,根据自己需要。

#import "LGPerson.h"
{
//需要设置为公共的,否者不能在外部访问。要在外部赋值这个实例变量必须加 @public 公有.KVO不能监听实例变量
    @public
    NSString *nikcName;
}
@interface LGStudent : LGPerson
+ (instancetype)shareInstance;
@end

//------------------------------------------------
#import "LGStudent.h"
@implementation LGStudent
static LGStudent* _instance = nil;
+ (instancetype)shareInstance{
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        _instance = [[super allocWithZone:NULL] init] ;
    }) ;
    return _instance ;
}
@end

//------------------------------------------------
#import "LJLKVOViewController.h"
#import "LGStudent.h"

@interface LJLKVOViewController ()
@property (nonatomic, strong) LGStudent *student;
@end

@implementation LJLKVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.student = [LGStudent shareInstance];
//1、注册观察者
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
//触摸事件修改student.name 触发监听回调
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.student.name = @"hello word";
//    外部访问实例变量 需要设置为公共
    self.student->nikcName = @"ljl";
}
#pragma mark - KVO回调
//2、实现回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    NSLog(@"LJLKVOViewController :%@",change);
}

//3、移除观察者
- (void)dealloc{
    [self.student removeObserver:self forKeyPath:@"name"];
}
@end

1.4、集合类型的属性

    对集合观察的时候必须建立在KVC 的基础之上。通过KVC来访问更加便利直接。
    LJLKVOPerson.h 添加一个可变数组属性

@property(nonatomic, strong) NSMutableArray * kvoArray;

注册KVO,并修改kvoArray

//    一定要初始化,否者 kvoArray为nil addObject:的时候就崩溃了
    self.person.kvoArray = [NSMutableArray array];
    [self.person addObserver:self
                  forKeyPath:@"kvoArray"
                     options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                     context:NULL];

    [self.person.kvoArray addObject:@"123"];
//    上面这样这样写是无法触发KVO 回调的,因为addObject:方法是不走setter 的。 而KVO监听是setter方法.需要将上述向数组添加元素的方法修改一下
    [[self.person mutableArrayValueForKey:@"kvoArray"] addObject:@"123"];
//    这样做之后 self.person.kvoArray 指向一个新的数组对象,相当于:
//    NSMutableArray * tmp = [NSMutableArray arrayWithArray:self.person.kvoArray];
//    [tmp addObject:@"123"];
//    self.person.kvoArray = tmp;
//    所以能触发KVO回调

1.5、多个相关属性的观察
    比如有一个 LJLKVODownloader 类,用来模拟下载,有三个属性totalBytes,completedBytes,和百分比进度progress:
    在UI层我们只关注 progress,但是进度是受其他两个属性影响,此时需要 LJLKVODownloader重写两个方法:

#import <Foundation/Foundation.h>

@interface LJLKVODownloader : NSObject
@property(nonatomic, assign) unsigned long long totalBytes;//总字节
@property(nonatomic, assign) unsigned long long completedBytes;//完成字节
@property(nonatomic, copy) NSString * progress;//进度
@end
#import "LJLKVODownloader.h"

@implementation LJLKVODownloader
//返回属性的一组键路径,这些属性的值会影响键控属性的值
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"progress"]) {
        NSArray * dependKeys = @[@"totalBytes",@"completedBytes"];
//        通过从数组中添加对象来设置
        keyPaths = [keyPaths setByAddingObjectsFromArray:dependKeys];
    }
    return keyPaths;
}

- (NSString *)progress{
    if (self.totalBytes == 0 || self.completedBytes == 0) {
        return @"0%";
    }
    double progress = (double)self.completedBytes/(double)self.totalBytes*100;
    if (progress > 100) {
        progress = 100;
    }
    return [NSString stringWithFormat:@"%d%%",(int)ceil(progress)];
}
@end
    LJLKVODownloader * downloader = [[LJLKVODownloader alloc] init];
    downloader.totalBytes = 205;
    [downloader addObserver:self
                 forKeyPath:@"progress"
                    options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                    context:NULL];
    downloader.completedBytes = 64;
    downloader.completedBytes = 168;

监听的回调结果如下

2020-03-08 00:34:53.547235+0800 filedome[31182:820584] change : {
    kind = 1;
    new = "32%";
    old = "0%";
}
2020-03-08 00:34:53.547581+0800 filedome[31182:820584] change : {
    kind = 1;
    new = "82%";
    old = "32%";
}

二、手动 or 自动观察开关

    在理解KVO之前呢,需要先理解KVC。
    KVC会在 setter 或 getter 进行调用,如果没有查找到,则调用类方法 +accessInstanceVariablesDirectly(直接访问实例变量),如果返回 YES ,再去查找成员变量
    KVO 也是类似的机制,在KVO接口中有这三个接口:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;(自动通知观察者)

+automaticallyNotifiesObserversForKey:默认返回YES,动态创建的中间类重写了setter,猜测在修改属性前后分别调用了
 -willChangeValueForKey:
 和 -didChangeValueForKey:类似方法,达到通知观察者的目的。
 如果子类中重载了+automaticallyNotifiesObserversForKey:并返回NO,则无法触发自动KVO通知机制,但我们可以通过手动调用-willChangeValueForKey:和-didChangeValueForKey:来触发KVO回调。

//    手动观察,不管是打开自动观察还是关闭自动观察都会回调
// 触发回调的是 didChangeValueForKey:
// 自动观察 也就是默认情况下,系统是帮我们添加了will 和 did 方法。如果在自己手动写一遍的话就会触发两次,也就是走一次did 方法就会触发一次回调

#import "LJLKVOPerson.h"
@implementation LJLKVOPerson

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

- (void)setName:(NSString *)name{
//    手动观察。不管是打开自动观察还是关闭自动观察都会回调
// 触发回调的是 didChangeValueForKey 
// 自动观察 也就是默认情况下,系统是帮我们添加了will 和 did 方法。如果在自己手动写一遍的话就会触发两次,也就是走一次did 方法就会触发一次回调
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];//这个方法里面触发回调
}
@end

三、KVO的实现原理

 KVO官方文档 中说KVO是使用  isa-swizzling  技术实现了键值的自动观察

  1. 动态生成子类 - NSKVONotifying_xxx
  2. 动态添加 setter 方法
  3. 动态添加 class 方法
  4. 动态添加 dealloc 方法
  5. 开启手动观察
  6. 消息转发给我们的原类   newValue
  7. 消息发送 - 响应回调方法

    2.2原理验证

验证一:

 self.person = [[LJLKVOPerson alloc] init];
//1、下一行下断点
 [self.person addObserver:self
               forKeyPath:@"name"
                  options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                  context:personNameContext];
//2、下一行下断点
 self.person.name = @"liujilou";

在上边代码标注的地方打断点然后进行LLBD调试

//    LLBD调试
//    1位置断点
(lldb) po object_getClassName(self.person)
"LJLKVOPerson"

//    走到2位置断点
(lldb) po object_getClassName(self.person)
"NSKVONotifying_LJLKVOPerson"

验证二:

    LJLKVOPerson * person = [[LJLKVOPerson alloc] init];
    [self printClasses:[LJLKVOPerson class]];
    [person addObserver:self
             forKeyPath:@"name"
                options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
                context:NULL];
    [self printClasses:[LJLKVOPerson class]];
#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++) {
//        判断cls 是否等于classes[i] 的父类
        if (cls == class_getSuperclass(classes[i])) {
//            将cls 的所有子类添加进来
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}
2020-03-07 22:31:31.788378+0800 filedome[29720:765937] classes = (
LJLKVOPerson
                                                                  )
2020-03-07 22:31:31.799907+0800 filedome[29720:765937] classes = (
LJLKVOPerson,
"NSKVONotifying_LJLKVOPerson"
                                                                  )
    

通过上面的两个验证,我们可以知道:

  1. 确实生成了一个中间类:NSKVONotifying_LJLKVOPerson
  2. NSKVONotifying_LJLKVOPerson 继承与 LJLKVOPerson
  3. 而且也把self.person 对象的 isa 指向了这个中间类。

开始研究子类,研究动态子类: isa superclass cache_t  bits - 方法 - 变量

下面继续验证后面的流程:

    self.person = [[LJLKVOPerson alloc] init];
    [self printClasses:[LJLKVOPerson class]];
    [self printClassAllMethod:[LJLKVOPerson class]];
    [self.person addObserver:self
             forKeyPath:@"name"
                options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
                context:NULL];
    [self printClasses:[LJLKVOPerson class]];
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LJLKVOPerson")];
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    NSLog(@"*********************");
    unsigned int count = 0;
//  需要导入  #import <objc/runtime.h>
    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);
}
    2020-03-07 22:59:18.260869+0800 filedome[30061:779679] classes = (
    LJLKVOPerson
                                                                      )
    2020-03-07 22:59:18.261029+0800 filedome[30061:779679] *********************
    2020-03-07 22:59:18.261129+0800 filedome[30061:779679] kvoArray-0x10b30e410
    2020-03-07 22:59:18.261252+0800 filedome[30061:779679] setKvoArray:-0x10b30e430
    2020-03-07 22:59:18.261348+0800 filedome[30061:779679] .cxx_destruct-0x10b30e470
    2020-03-07 22:59:18.261437+0800 filedome[30061:779679] name-0x10b30e3e0
    2020-03-07 22:59:18.261522+0800 filedome[30061:779679] setName:-0x10b30e340
    2020-03-07 22:59:18.272516+0800 filedome[30061:779679] classes = (
     LJLKVOPerson,
     "NSKVONotifying_LJLKVOPerson"
                                                                      )
    2020-03-07 22:59:18.272853+0800 filedome[30061:779679] *********************
    2020-03-07 22:59:18.273394+0800 filedome[30061:779679] setName:-0x10bfb0b5e
    2020-03-07 22:59:18.273505+0800 filedome[30061:779679] class-0x10bfaf592
    2020-03-07 22:59:18.273672+0800 filedome[30061:779679] dealloc-0x10bfaf336
    2020-03-07 22:59:18.273793+0800 filedome[30061:779679] _isKVOA-0x10bfaf32e

从上面的打印可以知道NSKVONotifying_LJLKVOPerson 重写了父类 LJLKVOPerson 的

  1. setName (setter)
  2. class
  3. dealloc
  4. _isKVOA

这几个方法。这个地方能打印出来的是自己实实在在有的方法。并不是自己没有打印父类的方法,所以能证明打印出来的这些方法都是进行了重写

重写 dealloc 方法来释放资源。

重写_isKVOA  这个私有方法是用来标示该类是一个 KVO 机制声称的类。


    观察的是 setter

        当一个类的实例第一次注册观察者是,系统会做以下事情
    1、动态生成一个继承自该类的中间类:NSKVONotifying_xxx
    2、修改原对象的 isa 指向这个中间类(isa-swizzling)
    3、子类中重写 -class 方法,依然返回原类,而非子类
    4、重写 -dealloc 方法
    5、重写 keypath 对应属性的 setter
    6、添加一个 -_isKVOA 方法

NSKVONotifying_xxx(LJLKVOPerson) 的这个 class 方法还是返回的是 LJLKVOPerson()。告诉我们操作的还是 LJLKVOPerson()

dealloc  在需要释放的地方调用释放中间类


移除观察 isa 是否回来 ?

移除观察的话isa 不再指向NSKVONotifying_xxx 由指回 xxx

验证:

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

在移除前后打断点然后通过控制台打印可以知道:

//在dealloc 移除观察者之前打断点
po object_getClassName(self.person)
"NSKVONotifying_LJLKVOPerson"

//移除观察者之后打断点,然后 LLBD
po object_getClassName(self.person)
"LJLKVOPerson"
//可以发现isa 又指向了 LJLKVOPerson

5:移除观察之后中间动态子类 是否销毁了?

不销毁,便于下次使用。如果每次都移除销毁,注册创建太慢。

验证:

在继承 ViewController 的 LJLKVOViewController 中写注册和观察等,然后返回 ViewController 页面的时候移除观察。

在ViewController 中添加如下代码。在移除观察者之后遍历 LJLKVOPerson 类的子类,看看是否有NSKVONotifying_LJLKVOPerson。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self printClasses:[LJLKVOPerson class]];
}
#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(@"LJLKVOViewController:classes = %@", mArray);
}
2020-03-08 01:40:23.120880+0800 002---KVO原理探讨[31915:843163] LJLKVOViewController:classes = (
    LJLKVOPerson,
    "NSKVONotifying_LJLKVOPerson",
    LGStudent
)

由此可知,在移除观察者之后,动态子类是不会被销毁的。

简单总结一下:

当一个类的实例第一次注册观察者是,系统会做以下事情
    1、动态生成一个继承自原类的中间类:NSKVONotifying_xxx
    2、修改原对象的 isa 指向这个中间类 NSKVONotifying_xxx(isa-swizzling)
    3、子类中重写  class 方法,依然返回原类,而非子类
    4、重写 dealloc 方法
    5、重写 keypath 对应属性的 setter (所以能观察属性,但是不能观察实例变量)
    6、添加一个  _isKVOA 方法


     KVO 的原理
     1: 动态生成子类 : NSKVONotifying_xxx
     2: 观察的是 setter (所以能观察属性,但是不能观察实例变量)
     3: 动态子类重写了很多方法 setNickName (setter)  、class 、dealloc 、_isKVOA
     4: 移除观察的时候 isa 指向回来
     5: 动态子类不会销毁

KVO的优缺点

优点:

  1. 能够提供一种简单的方法实现两个对象间的同步
  2. 能够对非我们创建的对象,即内部对象的状态改变做出响应,而且不需要改变内部对象的实现。能够提供观察的属性的最新值以及先前值。
  3. 用key path来观察属性,因此也可以观察嵌套对象
  4. 完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察

缺点:

  1. 因为观察的是 setter 方法所以只能观察属性不能观察实例变量
  2. 对属性重构将导致我们的观察代码不再可用
  3. 当释放观察者时需要移除观察者,否者或出现一些隐藏错误
发布了83 篇原创文章 · 获赞 12 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/104725189