一、概述

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. Android版APM地面站,支持直连和数传台连接

    现在隆重介绍APM上的手机/平板地面站 andropilot官方链接在此http://www.diydrones.com/groups/705844:Group:1132500?xg_source=m ...

  2. 【转】PHP如何快速读取大文件

    在PHP中,对于文件的读取时,最快捷的方式莫过于使用一些诸如file.file_get_contents之类的函数,简简单单的几行代码就能 很漂亮的完成我们所需要的功能.但当所操作的文件是一个比较大的 ...

  3. Binary Tree Inorder/Preorder Traversal 返回中序和前序/遍历二叉树的元素集合

    给定一个二叉树,以集合方式返回其中序/先序方式遍历的所有元素. 有两种方法,一种是经典的中序/先序方式的经典递归方式,另一种可以结合栈来实现非递归 Given a binary tree, retur ...

  4. Android 退出app,后台推送的服务也停止了,怎么可以做到不停止后台服务呢?

    service粘性等的那4种方式试了,三星的可以,小米老款手机可以,新款不行,华为新款也不行,还有魅族什么的,都不行,新款的手机上都有一个安全中心,只有在安全中心里面添加上允许app自启动才可以 怎么 ...

  5. js API列表

    //  主要是ES的API和一小部分浏览器的API. //  新加入标准的API有可能是浏览器事实上早已实现的. //   ECMAScript目前是每年都会发布新版本(目前已经相对稳定,每年都会又增 ...

  6. 使用Netty3或Netty4发布Http协议服务

    现在是2018年1月11日18:12分,已经是下班时间了,小Alan今天给大家简单的介绍一下Netty,让大家以后在使用到Netty的时候能够有一定的了解和基础,这样深入学习Netty以及以后灵活应用 ...

  7. spring boot(4)-html和templates

     静态页面 spring boot项目只有src目录,没有webapp目录,会将静态访问(html/图片等)映射到其自动配置的静态目录,如下 /static /public /resources ...

  8. C/C++内存对齐 ZZ

    这篇文章写得非常深入浅出.推荐.图需要到原博看. http://songlee24.github.io/2014/09/20/memory-alignment/ 下面是网易的一道笔试题:struct ...

  9. win8 便签工具

    启动或显示 Sticky Notes : Win+R--->StikyNot.exe 备份Sticky Notes 保存位置 : %AppData%\Microsoft\Sticky Notes ...

  10. 如何在CentOS 6.7上将PHP 5.3升级到PHP 5.6

    如何在CentOS 6.7上将PHP 5.3升级到PHP 5.6  andyz  博客  45评论   Facebook的TwitterGoogle+Reddit 在本文中,我将介绍如何在CentOS ...