一、概述

1.本文章内容,须参照本人的另一篇博客文章“class和object_getClass方法区别”加以理解;

2.基本使用:

//给实例对象instance添加观察者,监听该实例对象的某个属性值的变化

[self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"];

//监听值改变

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监测到%@的属性%@已经改变%@---%@", object, keyPath, change, context);
}

二、代码分析

1)添加kvo前后,类对象名称及其地址,以及setter方法地址对比:

//代码

- (void)test1
{
Class per1Class1 = object_getClass(self.per1);
Class per2Class2 = object_getClass(self.per2);
NSLog(@"添加kvo之前---className:%@ %@ %p %p", per1Class1, per2Class2, per1Class1, per2Class2);
NSLog(@"添加kvo之前---setter:%p %p", [self.per1 methodForSelector:@selector(setAge:)], [self.per2 methodForSelector:@selector(setAge:)]); //基本使用
[self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"]; Class per1Class3 = object_getClass(self.per1);
Class per2Class4 = object_getClass(self.per2);
NSLog(@"添加kvo之后---className:%@ %@ %p %p", per1Class3, per2Class4, per1Class3, per2Class4);
NSLog(@"添加kvo之后---setter:%p %p", [self.per1 methodForSelector:@selector(setAge:)], [self.per2 methodForSelector:@selector(setAge:)]);
}

//打印

-- ::48.099671+ KVONatureSearch[:] 添加kvo之前---className:Person Person 0x105cbe058 0x105cbe058
-- ::48.100035+ KVONatureSearch[:] 添加kvo之前---setter:0x105cbb5f0 0x105cbb5f0
-- ::48.101003+ KVONatureSearch[:] 添加kvo之后---className:NSKVONotifying_Person Person 0x6000016ec900 0x105cbe058
-- ::48.101183+ KVONatureSearch[:] 添加kvo之后---setter:0x1065a6cf2 0x105cbb5f0

分析:

1.self.per1添加kvo之前,类名为Person, 类对象地址为0x105cbe058,setter方法为0x105cbb5f0,self.per1和self.per2是一样的;

说明添加kvo之前,两个实例对象指向的是同一个类对象,而类对象在内存中仅有一份(仅创建一次);

2.添加之后,self.per2对象依然指向之前的类对象,而self.per1指向的是一个新的类对象NSKVONotifying_Person,同时setter方法的地址也不一样

2)添加kvo后,self.per1的isa指向的类对象的结构和self.per2的isa指向的类对象的机构的区别

//代码

- (void)test2
{
//基本使用
[self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"]; [self fetchClassMethods:object_getClass(self.per1)];
[self fetchClassMethods:object_getClass(self.per2)];
}

//打印

-- ::06.991414+ KVONatureSearch[:] NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,
-- ::06.991705+ KVONatureSearch[:] Person setAge:, age,

分析:

1.添加了kvo后的对象方法有四个:setAge:为属性的setter方法;class是一个对象方法(非类方法),这个方法有什么用,往后看;dealloc方法很好理解,销毁一些对象、内存管理等扫尾工作;_isKVOA字面理解为,即判断是否为kvo,返回一个BOOL值;

2.为什么NSKVONotifying_Person类对象中没有getter方法age,而只有setter方法setAge:,往下看;

//添加代码

struct lyb_objc_class *per1Class = (__bridge struct lyb_objc_class *)object_getClass(self.per1);

Class per2Class = object_getClass(self.per2);

Class per1Class2 = [self.per1 class];

//打断点

int i = 0;

//lldb模式:

(lldb) po per1Class
NSKVONotifying_Person (lldb) po per2Class
Person (lldb) p/x per2Class
(Class) $ = 0x00000001096ca178 Person
(lldb) p/x per1Class->super_class
(Class) $ = 0x00000001096ca178 Person

  (lldb) po per1Class2

Person

分析:

1.很显然NSKVONotifying_Person类对象的super_class指针指向的地址和Person类对象的地址是一样的,即NSKVONotifying_Person类是Person类的子类;

2.解决上述两个问题:

<1>NSKVONotifying_Person类为什么不复写getter方法age,因为没有需求,kvo的功能是监听属性值的改变,即对setter方法的监听,不需要监听getter方法,直接用父类的就行;

<2>class方法复写:问题是Person类并没有该方法,因为Person类继承自NSObject,点进去发现,class方法是NSObject的对象方法,如下图;

而上述class方法返回的类对象per1Class2却是Person,这就苹果公司复写class方法的原因:目的就是规避子类NSKVONotifying_Person暴露,让开发者认为实例对象添加kvo后依然是原来的类对象,所以class方法复写大概如下:

- (Class)class
{
return [Person class];
}

——其实本质上NSKVONotifying_Person类也是Person类,那为什么要动态创建这样一个子类呢,往下看;

3)实现原理:setter方法重写

//添加代码

- (void)test3
{
//基本使用
[self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"];
}

//打断点

//打印

-- ::28.687540+ KVONatureSearch[:] touchesBegan
-- ::28.688275+ KVONatureSearch[:] 监测到<Person: 0x6000013aece0>的属性age已经改变{
kind = ;
new = ;
old = ;
}---per1

分析:

1.很显然,只有per1被监听到了值的改变,而per2没有被监听;经断点调试,per1和per2两个对象值的改变,均调用了setAge:方法;所以Person类方法本身没有问题,跟kvo监听没有任何关系;

2.如上述分析,per1的类对象为NSKVONotifying_Person,而per2的类对象为Person;而NSKVONotifying_Person为Person的子类,并对setAge:方法进行了重写;那么kvo监听方法(

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context)只能是在重写的setAge:方法中被调用,具体是如何实现的,往下看;

//探寻setAge:方法复写

//更改代码

- (void)test3
{
NSLog(@"添加kvo之前---setAge:%p %p", [self.per1 methodForSelector:@selector(setAge:)], [self.per2 methodForSelector:@selector(setAge:)]); //基本使用
[self.per1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"per1"]; NSLog(@"添加kvo之后---setAge:%p %p", [self.per1 methodForSelector:@selector(setAge:)], [self.per2 methodForSelector:@selector(setAge:)]); //打断点
int i = ;
}

//lldb模式:

-- ::41.938211+ KVONatureSearch[:] 添加kvo之前---setter:0x1012524a0 0x1012524a0
-- ::41.938879+ KVONatureSearch[:] 添加kvo之后---setter:0x1015abcf2 0x1012524a0
(lldb) p (IMP)0x1012524a0
(IMP) $ = 0x00000001012524a0 (KVONatureSearch`-[Person setAge:] at Person.m:)
(lldb) p (IMP)0x1015abcf2
(IMP) $ = 0x00000001015abcf2 (Foundation`_NSSetIntValueAndNotify)
(lldb)

分析:

1.per1添加kvo之后,其isa指针指向系统创建的NSKVONotifying_ Person类对象,并复写了父类Person中的setAge:方法,该方法中只有一个方法即_NSSetIntValueAndNotify——该方法是Foundation框架中的方法,是一个C语言函数;

2.那么,也就是说,kvo的监听方法(- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context)是通过该方法来通知的;

3.所以问题来了,又是如何通知的呢,又怎么会调用父类的setAge:方法呢?

————原因:_NSSetIntValueAndNotify是苹果封装好了的,内部实现无法看到,但是可以通过反编译工具(因为Foundation包编译时会生成)Hopper和命令行的形式加以查看,此处涉及逆向工程、越狱等操作,在此不再赘述,有兴趣的可以自己上网查看!

经过上述操作:_NSSetIntValueAndNotify的内部实现大概如下:

//伪代码

void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}

//实现流程验证

//添加代码

#import "Person.h"

@implementation Person

- (void)setAge:(int)age
{
if (_age != age) {
_age = age;
} NSLog(@"setAge:");
} //void _NSSetIntValueAndNotify()
//{
// [self willChangeValueForKey:@"age"];
// [super setAge:age];
// [self didChangeValueForKey:@"age"];
//} - (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
} - (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey---begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey---end");
} @end

//打印

-- ::27.642672+ KVONatureSearch[:] touchesBegan
-- ::27.643455+ KVONatureSearch[:] willChangeValueForKey
-- ::27.643583+ KVONatureSearch[:] setAge:
-- ::27.643971+ KVONatureSearch[:] didChangeValueForKey---begin
-- ::27.644371+ KVONatureSearch[:] 监测到<Person: 0x6000018951d0>的属性age已经改变{
kind = ;
new = ;
old = ;
}---per1
-- ::27.644519+ KVONatureSearch[:] didChangeValueForKey---end

分析:

1.willChangeValueForKey:和didChangeValueForKey:是NSObject类的对象方法(类目扩展),用于监测实例对象属性值的改变;

2.我们通过在NSKVONotifying的父类Person中复写上述两个监听方法的结果可知,kvo监听方法的调用是在didChangeValueForKey:方法中进行——那么我们如何验证是不是在该方法中调用的呢,往下看;

//kvo监听方法调用验证

//改变代码

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
{
@public
int _age;
} //@property (nonatomic, assign) int age; @end NS_ASSUME_NONNULL_END
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"touchesBegan");
[self.per1 willChangeValueForKey:@"age"];
// self.per1.age = 20;
self.per1->_age = ;
// [self.per1 didChangeValueForKey:@"age"]; // self.per1.age = 30;
}

说明:

<1>此处不用@property来定义变量(我们知道,OC对象的本质是结构体,属性即为结构体中的成员,@property关键字在定义成员变量的同时,还为该成员变量在编译阶段由编译器自动生成setter和getter方法,当然如果程序中手动实现了setter和getter方法,则编译器会优先使用而不重新生成setter和getter方法——此处多说了两句,见谅!),而是直接定义成员变量;

<2>因为系统默认是@protected属性,所以需要设置成@public属性,才能供外部访问;

<3>此处不走setter方法,改用手动调用;

//打印

-- ::06.901408+ KVONatureSearch[:] touchesBegan
-- ::06.901698+ KVONatureSearch[:] willChangeValueForKey

分析:[self.per1 didChangeValueForKey:@"age"];把该句代码注释后,发现kvo的监听方法并没有触发,那么我们把它打开再看一下;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"touchesBegan");
[self.per1 willChangeValueForKey:@"age"];
// self.per1.age = 20;
self.per1->_age = ;
//打开了
[self.per1 didChangeValueForKey:@"age"]; // self.per1.age = 30;
}

//打印

-- ::24.961333+ KVONatureSearch[:] touchesBegan
-- ::24.961609+ KVONatureSearch[:] willChangeValueForKey
-- ::24.961901+ KVONatureSearch[:] didChangeValueForKey---begin
-- ::24.962212+ KVONatureSearch[:] 监测到<Person: 0x600003a10850>的属性age已经改变{
kind = ;
new = ;
old = ;
}---per1
-- ::24.962343+ KVONatureSearch[:] didChangeValueForKey---end

分析:kvo监听方法触发成功;

三、结论

kvo的本质:给实例对象添加观察者时,系统自动创建一个添加前该实例对象所属类的子类NSKVONotifying_Person,该子类通过对父类setter方法的复写(Foundation中封装的方法_NSSetXXXValueAndNotify,由C语言实现)来触发kvo的监听方法(- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context),实现流程为:

void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];//kvo的监听方法在该方法中被触发
}

//补充:

1.didChangeValueForKey:和willChangeValueForKey:跟_NSSetIntValueAndNotify()方法一样,是被苹果封装好了的,内部实现无法查看;

2.所谓的“手动触发kvo”:即直接用实例对象调用上述两个监听方法,值不改变(否则,就直接走setter方法了)

//代码

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"touchesBegan");
[self.per1 willChangeValueForKey:@"age"];
// self.per1.age = 20;
// self.per1->_age = 20;
//打开了
[self.per1 didChangeValueForKey:@"age"]; // self.per1.age = 30;
}

//打印

-- ::46.980240+ KVONatureSearch[:] touchesBegan
-- ::46.980597+ KVONatureSearch[:] willChangeValueForKey
-- ::46.980711+ KVONatureSearch[:] didChangeValueForKey---begin
-- ::46.981061+ KVONatureSearch[:] 监测到<Person: 0x6000027b0840>的属性age已经改变{
kind = ;
new = ;
old = ;
}---per1
-- ::46.981204+ KVONatureSearch[:] didChangeValueForKey---end

GitHub

kvo本质探寻的更多相关文章

  1. block本质探寻二之变量捕获

    一.代码 说明:本文章须结合文章<block本质探寻一之内存结构>和<class和object_getClass方法区别>加以理解: //main.m #import < ...

  2. block本质探寻八之循环引用

    说明:阅读本文,请参照之前的block文章加以理解: 一.循环引用的本质 //代码——ARC环境 void test1() { Person *per = [[Person alloc] init]; ...

  3. block本质探寻六之修改变量

    说明: <1>阅读本文章,请参照前面的block文章加以理解: <2>本文的变量指的是auto类型的局部变量(包括实例对象): <3>ARC和MRC两种模式均适用: ...

  4. block本质探寻四之copy

    说明: <1>阅读本文,最好阅读之前的block文章加以理解: <2>本文内容:三种block类型的copy情况(MRC).是否深拷贝.错误copy: 一.MRC模式下,三种b ...

  5. block本质探寻一之内存结构

    一.代码——命令行模式 //main.m #import <Foundation/Foundation.h> struct __block_impl { void *isa; int Fl ...

  6. KVC/KVO 本质

    KVO 的实现原理 KVO是关于runtime机制实现的 当某个类的对象属性第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter方法.派生类 ...

  7. block本质探寻七之内存管理

    说明: <1>阅读本问,请参照block前述文章加以理解: <2>环境:ARC: <3>变量类型:基本数据类型或者对象类型的auto局部变量: 一.三种情形 //代 ...

  8. block本质探寻五之atuto类型局部实例对象

    说明:阅读本文章,请参考之前的block文章加以理解: 一.栈区block分析 //代码 //ARC void test1() { { Person *per = [[Person alloc] in ...

  9. block本质探寻三之block类型

    一.oc代码 提示:看本文章之前,最好按顺序来看: //代码 void test1() { ; void(^block1)(void) = ^{ NSLog(@"block1----&quo ...

随机推荐

  1. django开发博客(1) 入门

    现在正式开始博客开发 1.安装django1.4 如果你使用的是fedoraDVD版,安装时选择了web开发组建,这一步可以省略,因为它自带django环境 django下载地址 https://ww ...

  2. ASP.NET MVC学习笔记 第二天

    创建视图      返回给客户端的HTML代码最好通过视图指定.视图都在Views文件夹中定义.ViewsDemo控制器的视图需要一个ViewsDemo子目录,这是视图的约定.      可以把多个控 ...

  3. Ubuntu 安装 PhpMyAdmin 图文教程

    Ubuntu 安装 PhpMyAdmin 管理 MySQL 数据库 PhpMyAdmin 是一个用 PHP 编写的软件工具,可以通过 web方式控制和操作 MySQL 数据库.通过 phpMyAdmi ...

  4. JavaScript 模块化入门

    理解模块 模块打包构建 webpack牛刀小试

  5. Project Euler 44: Find the smallest pair of pentagonal numbers whose sum and difference is pentagonal.

    In Problem 42 we dealt with triangular problems, in Problem 44 of Project Euler we deal with pentago ...

  6. asp.net生成PDF文件参考 .

    TextSharp 是用来生成  PDF 的一个组件,在 1998 年夏天的时候,Bruno Lowagie ,iText 的创作者,参与了学校的一个项目,当时使用 HTML 来生成报告,但是,使用 ...

  7. git 和github

    ssh git:    是一个版本管理工具,是可以在你电脑不联网的情况下,只在本地使用的一个版本管理工具,其作用就是可以让你更好的管理你的程序,比如你原来提交过的内容,以后虽然修改了,但是通过git这 ...

  8. bmp制作自定义字体(cocostudio使用)

    工具需求:bmpfont 1.步骤 (1)制作 * 把自己的字体放到一个txt文件中,写个脚本抽离出来, 重复了没有关系 * Edit->Select chars from fils(注意:Ed ...

  9. 计算机作业(Excel课程表) 物联网 王罗红

  10. [BZOJ 4036][HAOI2015]按位或

    4036: [HAOI2015]按位或 Time Limit: 10 Sec  Memory Limit: 256 MBSec  Special JudgeSubmit: 746  Solved: 4 ...