iOS底层探索21、KVO 原理

KVO 苹果文档地址
KVO: Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
键值编码是一种机制,允许在其他对象(被监听对象)的指定属性发生更改时被通知到监听对象。

一、KVO的简单使用

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor redColor];

    self.person = [MyPerson new];
    self.person.name = @"initName";
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    
    NSLog(@"newkey=%@ - oldKey=%@",change[NSKeyValueChangeNewKey],change[NSKeyValueChangeOldKey]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    self.person.name = [NSString stringWithFormat:@"%@+",self.person.name];
}

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

方法注释:

`- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;`
/* Register or deregister as an observer of the value at a key path relative to the receiver. 
      The options determine what is included in observer notifications and when they're sent, 
      as described above, and the context is passed in observer notifications as described above. 
      You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible 
      because it allows you to more precisely specify your intent. 
      When the same observer is registered for the same key path multiple times, 
      but with different context pointers each time, -removeObserver:forKeyPath: 
      has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/

1、context

- context:上下文,它是什么呢?--> 通过苹果文档和实际场景均可得知,context用来更安全精确便利 地指定和区分开我们的监听;
为何forKeyPath来区分不好?--> 当存在监听多个对象/多个属性,在处理监听时 仅仅判断监听的是哪个对象 就挺麻烦了,性能和代码可读性会变得复杂。
建议使用context.

2、removeObserver - 移除观察者

Removing an Object as an Observer 苹果文档

1)移除观察者
  1. observer不会在deallocated时自动移除;
  2. 使用-removeObserver:forKeyPath:context:
    通过注释:你应该尽可能使用-removeObserver:forKeyPath:context:而不是-removeObserver:forKeyPath:因为它允许你更精确地指定你的意图。当同一个观察者为相同的键路径多次注册,但每次都使用不同的上下文指针时,-removeObserver:forKeyPath:必须在决定删除什么内容时猜测上下文指针,而且它可能猜错了。
2)不移除观察者会出现的问题

创建MySubPerson单例代码如下:

@implementation MySubPerson

static MySubPerson* _instance = nil;
+ (instancetype)shareInstance{
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        _instance = [[super allocWithZone:NULL] init] ;
    }) ;
    return _instance ;
}
//- (void)personInstanceOne {
//    NSLog(@"%s",__func__);
//}

+ (void)personClassOne {
    NSLog(@"%s",__func__);
}

@end

分别在2(MyController1)、3(MyController2)级页面为其添加观察者,2级页面dealloc时不移除监听,运行工程:

kvo.gif

报错如下:
image.png

原因分析:
1:person是个MySubPerson单例对象,给它添加了2个监听,但是有一个未移除,person对象一直还在内存中;
2:再次进入页面又添加了一个监听,当属性改变时,需要给两个监听都发消息,但是只能找到现有的对象,原来的那个找不着了是个野指针了,它便导致崩溃。

3、开关 KVO

  1. 系统方法
    MyPerson类中将自动打开设为NO(默认是YES):
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

再次运行工程,监听不到属性变化了。

  1. 手动打开
    代码如下:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}
- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

再次运行工程,可正常监听到变化。

4、监听集合类型

以数组为例.

 // 数组
    self.person.mDataArray = [NSMutableArray array];
    [self.person addObserver:self forKeyPath:@"mDataArray" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    
    NSLog(@"newkey=%@ - oldKey=%@",change[NSKeyValueChangeNewKey],change[NSKeyValueChangeOldKey]);
}


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
//    self.person.name = [NSString stringWithFormat:@"%@-",self.person.name];
    [self.person.mDataArray addObject:@"one"];
}

并未监听到变化,查看苹果文档所给信息:

iOS底层探索21、KVO 原理_第1张图片
image.png

KVO是基于KVC的,我们对代码进行如下修改:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    self.person.name = [NSString stringWithFormat:@"%@-",self.person.name];
    [[self.person mutableArrayValueForKey:@"mDataArray"] addObject:@"one"]; //[self.person.mDataArray addObject:@"one"];
}

运行工程,监听结果:

2020-10-28 18:47:03.337414+0800 DemoEmpty_iOS[9449:394437] {
    kind = 1;// NSKeyValueChangeSetting
    new = "(null)-";
    old = "";
}
2020-10-28 18:47:03.338002+0800 DemoEmpty_iOS[9449:394437] {
    indexes = "<_NSCachedIndexSet: 0x60000396e700>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;// NSKeyValueChangeInsertion
    new =     (
        one
    );
}

KVO部分代码:

/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};

typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
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));

二、 KVO 底层原理

1、属性和成员

MyController2 中主要代码如下:

self.person = [[MyPerson alloc]init];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
    [self.person addObserver:self forKeyPath:@"ivarName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSLog(@"赋值前 name=%@ - ivarName=%@",self.person.name,self.person->ivarName);

    self.person.name = @"名字";
    self.person->ivarName = @"昵称";
    
    NSLog(@"赋值后 name=%@ / ivarName=%@",self.person.name,self.person->ivarName);
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"observer %@",change);
    NSLog(@"newkey=%@ / oldKey=%@",change[NSKeyValueChangeNewKey],change[NSKeyValueChangeOldKey]);
}

运行工程,输出:

2020-10-28 23:07:35.530041+0800 DemoEmpty_iOS[9847:515045] 赋值前 name=(null) - ivarName=(null)
2020-10-28 23:07:35.530594+0800 DemoEmpty_iOS[9847:515045] observer {
    kind = 1;
    new = "\U540d\U5b57";
    old = "";
}
2020-10-28 23:07:35.530846+0800 DemoEmpty_iOS[9847:515045] newkey=名字 / oldKey=
2020-10-28 23:07:35.531069+0800 DemoEmpty_iOS[9847:515045] 赋值后 name=名字 / ivarName=昵称

KVO只对属性进行观察 --> setter 方法。
下面对KVO原理进行探究。

2、KVO原理

1)运行工程,查看self.personisa:

self.person = [[MyPerson alloc]init];// MyPerson
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];// NSKVONotifying_MyPerson
/*
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x000000010d02317a "MyPerson"
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $2 = 0x00006000023768e0 "NSKVONotifying_MyPerson"
*/

添加KVO监听后,self.personisa指向由MyPerson变成了一个中间类 - NSKVONotifying_MyPerson.NSKVONotifying_MyPerson是个什么类?
在监听前后分别输出打印下所有类子类 与 方法:

#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls {
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];// 先把 cls 自己放进去
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i

输出结果:

2020-10-28 23:30:45.280579+0800 DemoEmpty_iOS[9977:527858] controller:classes = (
    MyPerson,
    MySubPerson
)
2020-10-28 23:30:51.984501+0800 DemoEmpty_iOS[9977:527858] controller:classes = (
    MyPerson,
    "NSKVONotifying_MyPerson",
    MySubPerson
)

NSKVONotifying_MyPersonMyPerson的一个派生子类。

2)MyPersonNSKVONotifying_MyPerson的方法

2.1、分别输出2者的全部方法
#pragma mark - 遍历方法   - ivar - property
- (void)printClassAllMethod:(Class)cls {
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i

MyPerson 的所有方法:

2020-10-28 23:42:31.320759+0800 DemoEmpty_iOS[10015:533265] nick-0x10bc2aa50
2020-10-28 23:42:31.320985+0800 DemoEmpty_iOS[10015:533265] setNick:-0x10bc2aa80
2020-10-28 23:42:31.321095+0800 DemoEmpty_iOS[10015:533265] personInstanceOne-0x10bc2a980
2020-10-28 23:42:31.321274+0800 DemoEmpty_iOS[10015:533265] mDataArray-0x10bc2aac0
2020-10-28 23:42:31.321421+0800 DemoEmpty_iOS[10015:533265] setMDataArray:-0x10bc2aae0
2020-10-28 23:42:31.321563+0800 DemoEmpty_iOS[10015:533265] .cxx_destruct-0x10bc2ab60
2020-10-28 23:42:31.321688+0800 DemoEmpty_iOS[10015:533265] name-0x10bc2a9e0
2020-10-28 23:42:31.321800+0800 DemoEmpty_iOS[10015:533265] setName:-0x10bc2aa10
2020-10-28 23:42:31.321922+0800 DemoEmpty_iOS[10015:533265] age-0x10bc2ab20
2020-10-28 23:42:31.322032+0800 DemoEmpty_iOS[10015:533265] setAge:-0x10bc2ab40

NSKVONotifying_MyPerson 的所有方法:

2020-10-28 23:50:47.534742+0800 DemoEmpty_iOS[10055:538121] setName:-0x7fff25721c7a
2020-10-28 23:50:47.534892+0800 DemoEmpty_iOS[10055:538121] class-0x7fff2572073d
2020-10-28 23:50:47.534990+0800 DemoEmpty_iOS[10055:538121] dealloc-0x7fff257204a2
2020-10-28 23:50:47.535088+0800 DemoEmpty_iOS[10055:538121] _isKVOA-0x7fff2572049a
2.2、问题:NSKVONotifying_MyPerson是继承还是重写呢?

新建继承自MyPerson的类MySubPerson,输出其全部方法
--> 没有任何输出!
--> 在MySubPerson中实现任一方法(例:setNick:),输出结果如下:

2020-10-29 00:08:21.050077+0800 DemoEmpty_iOS[10196:549729] setNick:-0x103515530

对比NSKVONotifying_MyPerson,完全不同。
可知NSKVONotifying_MyPerson的方法不是继承来的,而是自己重写的,且方法一定是有实现的。

3)setName

问题:setter方法是如何重写了父类的属性的呢?

3.1、添加观察点

addObserver前断点,给观察的属性name添加观察点 watchpoint set variable self->_person->_name,添加成功:

(lldb) watchpoint set variable self->_person->_name
Watchpoint created: Watchpoint 1: addr = 0x60000377d630 size = 8 state = enabled type = w
    watchpoint spec = 'self->_person->_name'
    new value: 0x0000000000000000

放开断点,继续执行,当name发生变化时,watchpoint 1 hit

Watchpoint 1 hit:
old value: 0x0000000000000000
new value: 0x000000010b493108

此时堆栈信息见下图:

image.png

查看2、3、4的内容信息:
2中信息如下:

image.png

更详细内容:

    0x7fff257273ea <+457>: leaq   -0x3a7e(%rip), %r8        ; NSKeyValueWillChangeBySetting
    0x7fff257273f1 <+464>: movq   -0x590(%rbp), %r9
    0x7fff257273f8 <+471>: pushq  $0x0
    0x7fff257273fa <+473>: pushq  %rbx
    0x7fff257273fb <+474>: leaq   0x4da(%rip), %rax         ; NSKeyValuePushPendingNotificationLocal
    0x7fff25727402 <+481>: pushq  %rax
    0x7fff25727403 <+482>: callq  0x7fff257275d2            ; NSKeyValueWillChange
    0x7fff25727408 <+487>: addq   $0x20, %rsp
    0x7fff2572740c <+491>: incq   %r12
    0x7fff2572740f <+494>: cmpq   %r12, %r14
    0x7fff25727412 <+497>: jne    0x7fff257273c6            ; <+421>
    0x7fff25727414 <+499>: cmpq   $0x0, -0x570(%rbp)
    0x7fff2572741c <+507>: je     0x7fff2572747f            ; <+606>
    0x7fff2572741e <+509>: movb   $0x0, -0x540(%rbp)
    0x7fff25727425 <+516>: decq   %r14
    0x7fff25727428 <+519>: js     0x7fff2572747f            ; <+606>
    0x7fff2572742a <+521>: leaq   -0x560(%rbp), %rbx
    0x7fff25727431 <+528>: leaq   -0x3ac5(%rip), %r12       ; NSKeyValueWillChangeBySetting
    0x7fff25727438 <+535>: movq   -0x598(%rbp), %rax
... 信息太多 省略一部分 ...
    0x7fff25727469 <+584>: leaq   0x46c(%rip), %rax         ; NSKeyValuePushPendingNotificationLocal
    0x7fff25727470 <+591>: pushq  %rax
    0x7fff25727471 <+592>: callq  0x7fff257275d2            ; NSKeyValueWillChange
    0x7fff25727476 <+597>: addq   $0x20, %rsp
    0x7fff2572747a <+601>: decq   %r14
    0x7fff2572747d <+604>: jns    0x7fff25727438            ; <+535>
    0x7fff2572747f <+606>: movq   -0x550(%rbp), %rax
    0x7fff25727486 <+613>: movq   %rax, -0x578(%rbp)
    0x7fff2572748d <+620>: movq   -0x548(%rbp), %r14
    0x7fff25727494 <+627>: movq   -0x588(%rbp), %rbx
    0x7fff2572749b <+634>: movq   0x10(%rbp), %rdi
    0x7fff2572749f <+638>: testq  %rdi, %rdi
    0x7fff257274a2 <+641>: je     0x7fff257274a7            ; <+646>
    0x7fff257274a4 <+643>: callq  *0x10(%rdi)
->  0x7fff257274a7 <+646>: testq  %r14, %r14
    0x7fff257274aa <+649>: jle    0x7fff2572750a            ; <+745>
    0x7fff257274ac <+651>: leaq   -0x560(%rbp), %rax
    0x7fff257274b3 <+658>: movq   -0x578(%rbp), %rcx
    0x7fff257274ba <+665>: movq   %rcx, (%rax)
    0x7fff257274bd <+668>: movq   %r14, 0x8(%rax)
    0x7fff257274c1 <+672>: xorl   %ecx, %ecx
    0x7fff257274c3 <+674>: movq   %rcx, 0x20(%rax)
    0x7fff257274c7 <+678>: movq   %rcx, 0x18(%rax)
    0x7fff257274cb <+682>: movq   %rcx, 0x10(%rax)
    0x7fff257274cf <+686>: movq   -0x568(%rbp), %rcx
    0x7fff257274d6 <+693>: movq   %rcx, 0x28(%rax)
    0x7fff257274da <+697>: subq   $0x8, %rsp
    0x7fff257274de <+701>: leaq   -0x39ff(%rip), %rcx       ; NSKeyValueDidChangeBySetting
    0x7fff257274e5 <+708>: leaq   0x77f(%rip), %r9          ; NSKeyValuePopPendingNotificationLocal
... 信息太多 省略一部分 ...
    0x7fff25727501 <+736>: callq  0x7fff25727a10            ; NSKeyValueDidChange
    0x7fff25727506 <+741>: addq   $0x10, %rsp
    0x7fff2572750a <+745>: movq   -0x568(%rbp), %rdi
    0x7fff25727511 <+752>: callq  *0x5b23a951(%rip)         ; (void *)0x00007fff51411000: objc_release
    0x7fff25727517 <+758>: testq  %r13, %r13
    0x7fff2572751a <+761>: je     0x7fff25727534            ; <+787>
    0x7fff2572751c <+763>: xorl   %ebx, %ebx
    0x7fff2572751e <+765>: movq   0x5b23a943(%rip), %r14    ; (void *)0x00007fff51411000: objc_release
    0x7fff25727525 <+772>: movq   (%r15,%rbx,8), %rdi

通过上面信息可知,断点在0x7fff257274a7 <+646>: testq %r14, %r14处,在此前后流程,汇编代码流程中分别有 NSKeyValueWillChangeNSKeyValueDidChange,而NSKeyValueDidChange前又走进了父类的setter方法;
可得知 流程 -->NSKeyValueWillChange--> 父类[MyPerson setName]-->NSKeyValueDidChange-->objc_release
这里即可验证父类的属性值为何会变化。

4)问:NSKVONotifying_MyPerson会被移除吗?isa是否会指回MyPerson?

- (void)dealloc {
    [self printClassAllMethod:objc_getClass("NSKVONotifying_MyPerson")];

    [self.person removeObserver:self forKeyPath:@"name" context:NULL];

    [self printClassAllMethod:objc_getClass("NSKVONotifying_MyPerson")];
}

析构函数中的removeObserver前后依次输出信息如下:

2020-10-29 00:29:47.516214+0800 DemoEmpty_iOS[10221:558663] setName:-0x7fff25721c7a
2020-10-29 00:29:47.516397+0800 DemoEmpty_iOS[10221:558663] class-0x7fff2572073d
2020-10-29 00:29:47.516561+0800 DemoEmpty_iOS[10221:558663] dealloc-0x7fff257204a2
2020-10-29 00:29:47.516654+0800 DemoEmpty_iOS[10221:558663] _isKVOA-0x7fff2572049a

(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $0 = 0x000060000324bc00 "NSKVONotifying_MyPerson"
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x000000010e1ca17c "MyPerson"

2020-10-29 00:30:03.485147+0800 DemoEmpty_iOS[10221:558663] setName:-0x7fff25721c7a
2020-10-29 00:30:03.485293+0800 DemoEmpty_iOS[10221:558663] class-0x7fff2572073d
2020-10-29 00:30:03.485441+0800 DemoEmpty_iOS[10221:558663] dealloc-0x7fff257204a2
2020-10-29 00:30:03.485540+0800 DemoEmpty_iOS[10221:558663] _isKVOA-0x7fff2572049a
(lldb) 

返回到其他页面再次输出,NSKVONotifying_MyPerson仍是存在的。
答:由上可得知,在移除监听的观察者后,isa指回了MyPerson。而NSKVONotifying_MyPerson会一直在内存中并不会移除。这里若下次再进来不必再次开辟加载,节省了时间性能。
KVO Demo 地址

总结:

  1. KVO 的实现使用了isa-swizzing
  2. 中间类NSKVONotifying_clsNameclsName的子类,并重写了方法;
  3. NSKVONotifying_clsName不会被销毁,一直存在在内存中,生命和普通类相同;
  4. 析构 removeObserver后,对象的isa会指回clsName

三、自定义 KVO

模拟系统,自定义实现 KVO
思路:

  1. 验证是否存在setter方法,处理实例对象不监听
  2. 动态生成子类 - NSKVONotifying_XXX
    2.1 申请类
    2.2 注册
    2.3 添加方法
  3. isa 指向
  4. 父类 setter
  5. 观察者去响应
  6. removeObserver
  • Demo 地址

你可能感兴趣的:(iOS底层探索21、KVO 原理)