mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 04:17:17 +00:00
feature: dyld && LD 链接器
This commit is contained in:
@@ -1,6 +1,282 @@
|
||||
# KVC && KVO
|
||||
|
||||
## 一、基本用法-字典快速赋值
|
||||
> KVO 的实现原理是什么?如何手动触发 KVO?本文来探索下 iOS 中 KVO 底层细节
|
||||
|
||||
|
||||
|
||||
## 底层实现分析
|
||||
|
||||
Demo1:创建 Person 类,点击事件里触发属性值的改变。
|
||||
|
||||
<img src="./../assets/XCodoKVOIsaInspect.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
分析:
|
||||
|
||||
- 添加过 KVO 的 person1,isa 为系统利用 Runtime 技术动态创建的类,名字为 `NSKVONotifying_Person`
|
||||
- 没有添加过 KVO 的 person2,isa 为 Person 的类对象
|
||||
- `.height = 177` 本质是 `[self.person1 setHeight:177]` 也就是调用 set 方法。
|
||||
|
||||
在内存中的结构如下图
|
||||
|
||||
<img src="./../assets/UnusedKVOIsaStructure.png" style="zoom:25%">
|
||||
|
||||
整个流程分析下:
|
||||
|
||||
- self.person2 调用 setHeight 的时候,首先根据 self.person2 实例对象的 isa 找到 Person 类对象,然后在方法列表中找到 setHeight 方法,然后进行调用
|
||||
- self.person1 调用 setHeight 的时候,首先根据 self.person1 实例对象的 isa 找到 `NSKVONotifying_Person` 类对象,然后在方法列表中找到 setHeight 方法,然后进行调用。内部实现中,会调用 Foundation 的 `_NSSetIntValueAndNotify` 方法。
|
||||
- 然后调用: willSet、super setHeight、didSet 方法。
|
||||
|
||||
当我们按照 KVO 后动态生成的类名去创建一个新的类的时候,Xcode 会报错:`[general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class`。因为自己创建的类名和系统将要动态创建的类名冲突了,并且 KVO 监听失效
|
||||
|
||||
<img src="./../assets/SelfClassNameConflictsWithKVOClass.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
Demo2:
|
||||
|
||||
<img src="./../assets/KVOMethodImplAddressAndIsaInspect.png" style="zoom:25%">
|
||||
|
||||
分析:
|
||||
|
||||
- 可以看到在对 self.person1 添加 KVO 之后,self.person1 的类对象改变了,也就是 self.person1的 isa 改变了,变为系统动态生成的新类
|
||||
- 在对 self.person1 添加 KVO 之后,self.person1 的 setHeight 方法的实现变了,添加前后 self.person2 的 setHeight 方法,都是 Person 类对象的 setHeight 方法实现。KVO 添加前后都未改变
|
||||
- 利用 `(IMP)方法地址` 查看,没有进行 KVO 的 `setHeight` 是在 `KVOExplore -[Person setHeight:] at Person.h:14)` 里。添加过 KVO 的是在 `Foundation _NSSetLongLongValueAndNotify` 里。且 `_NSSetLongLongValueAndNotify`是个 c 语言函数。
|
||||
|
||||
|
||||
|
||||
Demo3:
|
||||
|
||||
<img src="./../assets/KVOMethodImplAddressWithDouble.png" style="zoom:25%">
|
||||
|
||||
可以看到我们将 KVO 的数据类型改为 double 后,原本的 setHeight 是 `_NSSetLongLongValueAndNotify`,现在是 `_NSSetDoubleValueAndNotify`
|
||||
|
||||
也可以借助于 `nm` 来查看所有 Foundation 关于 KVO 的方法 `nm Foundation | grep ValueAndNotify ` (需要自己提取真机上的 Foundation 符号表)
|
||||
|
||||
|
||||
|
||||
### NSSet**ValueAndNotify 的内部实现
|
||||
|
||||
<img src="./../assets/UsedKVOIsaStructure.png" style="zoom:25%">
|
||||
|
||||
来对 Person 类增加一些打印方法
|
||||
|
||||
<img src="./../assets/KVOKeyMethodPrint.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
可以看出内部实现是:
|
||||
|
||||
- 调用 `willChangeVlueForKey`
|
||||
|
||||
- 调用原本的 setter
|
||||
|
||||
- 调用 `didChangeValueForKey`。在 `didChangeValueForKey` 内部会调用 KVO 的 `- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context` 方法
|
||||
|
||||
|
||||
|
||||
```objective-c
|
||||
@interface Person()
|
||||
@property (nonatomic, assign) double height;
|
||||
@end
|
||||
@implementation Person
|
||||
- (void)setHeight:(double)height {
|
||||
_height = height;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface NSKVONotifying_Person: Person()
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSKVONotifying_Person
|
||||
- (void)setHeight:(double)height {
|
||||
[self setDoubleValueAndNotify:height];
|
||||
}
|
||||
|
||||
- (void)setDoubleValueAndNotify:(double)height {
|
||||
[self willChangeValueForKey:@"height"];
|
||||
[super setHeight: height];
|
||||
[self didChangeValueForKey:@"height"];
|
||||
}
|
||||
|
||||
- (void)didChangeValueForKey:(NSString *)key {
|
||||
[observer observeValueForKeyPath:key ofObject:self change:@{} context:nil];
|
||||
}
|
||||
|
||||
- (Class)class {
|
||||
return [Person class];
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 重写 class 方法
|
||||
|
||||
<img src="./../assets/KVOOverrrideClassMethod.png" style="zoom:25%">
|
||||
|
||||
可以看到利用 runtime api,在添加 KVO 之后,类对象为 `NSKVONotifying_Person`
|
||||
|
||||
但是利用 class 方法,添加 KVO 之后,获取类对象依旧为 Person。
|
||||
|
||||
好处是:屏蔽了 KVO 底层内部实现,隐藏了 `NSKVONotifying_Person` 的存在,通过 `-(Class)class` 方法告诉开发者添加 KVO 之后的类,依旧是 Person,本质上是继承自 Person 的类对象,能力没有改变。
|
||||
|
||||
|
||||
|
||||
### KVO 类的所有方法
|
||||
|
||||
<img src="./../assets/PrintKVOClassAllMethodName.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
利用 runtime api,打印添加 KVO 后,动态创建的 NSKVONotifying_Person 都存在什么方法?
|
||||
|
||||
- setHeight:
|
||||
- class
|
||||
- dealloc
|
||||
- _isKVOA
|
||||
|
||||
QA:为什么新创建的类没有 getter?
|
||||
|
||||
因为新创建的类是子类,父类中存在 getter。子类中增加的方法只是为了触发 KVO,getter 不影响。
|
||||
|
||||
|
||||
|
||||
### 修改成员变量的值可以触发 KVO 吗
|
||||
|
||||
<img src="./../assets/KVOCannotInvokeWhenChangeIvarDirectly.png" style="zoom:25%">
|
||||
|
||||
我们将 Person 类的成员变量暴露出来,在点击事件里修改,发现不能触发 KVO。
|
||||
|
||||
也就是说,触发 KVO 的本质是必须要有 setter,且触发 setter。直接修改成员变量,是不会触发 setter 的。
|
||||
|
||||
QA:如何手动触发?
|
||||
|
||||
手动调用 `willChangeValueForKey`、 `didChangeValueForKey`
|
||||
|
||||
<img src="./../assets/KVOInvokeWhenChangeIvarDirectlyMustCallWillChangeAndDidChange.png" style="zoom:25%">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
QA:请描述系统如何实现一个对象的 KVO?KVO 的本质是什么
|
||||
|
||||
- 系统利用 runtime 的能力,动态创建一个监听对象的类的子类,子类命名格式为: `NSKVONotifying_类名`。 并且让 instance 对象的 isa 指向这个全新的子类
|
||||
- 当修改 instance 对象的属性时,会调用 Foundation 框架的 `_NSSet***ValueAndNotify` c 函数
|
||||
- 然后调用:
|
||||
- `willChangeValueForKey`
|
||||
- 原来的 setter
|
||||
- `didChangeValueForKey`,且 didChangeValueForKey 内部会触发监听器(Observer)的监听方法(`- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context`)。也就是发消息
|
||||
|
||||
QA:如何手动触发 KVO?
|
||||
|
||||
上面剖析了 KVO 的系统实现,所以手动触发 KVO 就需要调用 `willChangeValueForKey` 和 `didChangeValueForKey`
|
||||
|
||||
|
||||
|
||||
### 当没有observer观察任何一个property时,删除动态创建的子类
|
||||
|
||||
`[self.person removeObserver:self forKeyPath:@"height"];` 该代码调用后,会删除动态创建的子类。
|
||||
|
||||
|
||||
|
||||
## KVC
|
||||
|
||||
`setValueForKey` 用来设置对象的一层属性值修改。
|
||||
|
||||
`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
|
||||
|
||||
|
||||
|
||||
### KVC 设值原理
|
||||
|
||||
KVC 之后会触发 KVO 吗?
|
||||
|
||||
<img src="./../assets/KVCWillTriggerKVO.png" style="zoom:25%">
|
||||
|
||||
发现 KVC 触发了 KVO。
|
||||
|
||||
问题来了:为什么 KVC 会触发 KVO?探究下 `setValueForKey`
|
||||
|
||||
整个流程如下
|
||||
|
||||
<img src="./../assets/KVC-process.png" style="zoom:45%">
|
||||
|
||||
`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 `_key`、`_isKey`、`key`、`isKey` 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 `NSUnknownKeyException`
|
||||
|
||||
```
|
||||
@implementation Person
|
||||
- (void)setAge:(int)age
|
||||
{
|
||||
_age = age;
|
||||
}
|
||||
|
||||
- (void)_setAge:(int)age
|
||||
{
|
||||
_age = age;
|
||||
}
|
||||
|
||||
+ (BOOL)accessInstanceVariableDirectlt
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 直接修改成员变量会触发 KVO 吗?
|
||||
|
||||
不会。KVO 的实现原理就是在 setter 方法内,调用 `willChangeValueForKey`、`didChangeValueForKey` ,所以直接修改成员变量不会触发 KVO。
|
||||
|
||||
想要触发,可以手动调用上面2个 API。
|
||||
|
||||
```objective-c
|
||||
- (void)setName:(NSString *)name {
|
||||
[self willChangeValueForKey:@"name"];
|
||||
_name = name;
|
||||
[self didChangeValueForKey:@"name"];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### KVC 取值原理
|
||||
|
||||
`valueForKey` 原理
|
||||
|
||||
- 按照 getKey、key、isKey、_key 的顺序寻找方法实现,找到则直接调用方法,返回值
|
||||
- 如果没找到则调用 `+(BOOL)accessInstanceVariableDirectly` 方法,询问是否可以访问成员变量
|
||||
- 为 NO 则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
|
||||
- 为 YES 则按照 ` __key`、`_isKey`、`key`、`isKey` 的顺序访问成员变量。找到哪个则返回值
|
||||
|
||||
- 都没找到则调用 `valueForUndefinedKey `并抛出 `NSUnknownKeyException`异常
|
||||
|
||||
|
||||
|
||||
<img src="./../assets/KVC-get-process.png" style="zoom:45%">
|
||||
|
||||
|
||||
|
||||
### KVC 会破坏面向对象的原则吗?
|
||||
|
||||
KVC 有违背面向对象编程思想吗?如果一个类的成员变量是私有的,也没有在 `.h` 中公开一些方法去设置、修改成员变量,那么外部直接通过 KVC 去修改值,是有违背面向对象编程思想的。
|
||||
|
||||
KVC 提供了对应的能力,去保护或者说支持面向对象的原则。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 基本用法-字典快速赋值
|
||||
|
||||
KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setValuesForKeysWithDictionary`。
|
||||
|
||||
@@ -84,11 +360,11 @@ self.person.dog.weight = 50;
|
||||
}
|
||||
```
|
||||
|
||||
## 二、 KVO 的本质
|
||||
|
||||
kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础。
|
||||
|
||||
### 几个基本的知识点
|
||||
|
||||
|
||||
## 几个基本的知识点
|
||||
|
||||
1. KVO 观察者和属性被观察的对象之间不是强引用的关系
|
||||
|
||||
@@ -127,15 +403,11 @@ kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础
|
||||
@end
|
||||
```
|
||||
|
||||
8. 手动触发 KVO?调用 willChangeValueForKey、didChangeValueForKey
|
||||
|
||||
9.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 三、 实现机制
|
||||
## 实现机制
|
||||
|
||||
> Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
|
||||
|
||||
@@ -145,22 +417,43 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
|
||||
|
||||
- KVO 是基于 Runtime 机制实现的
|
||||
|
||||
- 当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现`通知机制`
|
||||
- 当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类(子类)。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现`通知机制`
|
||||
|
||||
- 如果当前类为 Person,则生成的派生类名称为 `NSKVONotifying_Person`
|
||||
- 如果当前类为 Person,则生成的派生(子类)类名称为 `NSKVONotifying_Person`
|
||||
|
||||
- 每个类对象中都有一个 `isa指针` 指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的 `setter` 方法
|
||||
- 每个类对象中都有一个 `isa指针` 指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的 `setter` 方法
|
||||
|
||||
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。
|
||||
|
||||

|
||||

|
||||
|
||||
为什么要选择是继承的子类而不是分类呢?
|
||||
子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃
|
||||
|
||||
关于分类与子类的关系可以看看我之前的 [文章](1.50.md).
|
||||
|
||||
## 四、 模拟实现系统的 KVO
|
||||
|
||||
|
||||
## QA
|
||||
|
||||
iOS 用什么方式实现对一个对象的 KVO?(KVO 的本质是什么)
|
||||
|
||||
- 当对一个对象使用了 KVO 监听,系统会修改这个对象的 isa 指针,改为一个指向通过 Runtime 动态创建的子类
|
||||
- 子类拥有自己对监听属性的 setter 实现,内部会调用
|
||||
- willChangeValueForKey
|
||||
- 原来的 setter 实现
|
||||
- didChangeValueForKey,这个方法内部又会调用监听器的 监听方法 `[observer observeValueForKey:ofObject:change:context:]`
|
||||
|
||||
如何手动触发 KVO?
|
||||
|
||||
```objective-c
|
||||
[p willChangeValueForKey:@"height"];
|
||||
[p didChangeValueForKey:@"height"];
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 模拟实现系统的 KVO
|
||||
|
||||
1. 创建被观察对象的子类
|
||||
2. 重写观察对象属性的 set 方法,同时调用 `willChangeValueForKey、didChangeValueForKey`
|
||||
@@ -267,54 +560,3 @@ KVO 的改装:
|
||||
|
||||
|
||||
|
||||
## 五、 KVC
|
||||
|
||||
`setValueForKey` 用来设置对象的一层属性值修改。
|
||||
|
||||
`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
|
||||
|
||||
|
||||
|
||||
KVC 之后会触发 KVO。为什么?探究下 `setValueForKey`
|
||||
|
||||
`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES,则可以直接修改成员变量的值,会按照 _key、_isKey、key、isKey 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 NSUnknownKeyException
|
||||
|
||||
整个流程如下
|
||||
|
||||

|
||||
|
||||
```
|
||||
@implementation Person
|
||||
- (void)setAge:(int)age
|
||||
{
|
||||
_age = age;
|
||||
}
|
||||
|
||||
- (void)_setAge:(int)age
|
||||
{
|
||||
_age = age;
|
||||
}
|
||||
|
||||
+ (BOOL)accessInstanceVariableDirectlt
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
valueForKey 原理
|
||||
|
||||
- 按照 getKey、key、isKey、_key 的顺序寻找方法实现
|
||||
|
||||
- 找到则直接调用方法,返回值
|
||||
|
||||
- 如果没找到则调用 accessInstanceVariableDirectlt 方法,询问是否可以访问成员变量。为 NO 则抛出异常
|
||||
|
||||
- 为 YES 则按照 __key、isKey、key、isKey 的顺序访问成员变量。找到哪个则返回值
|
||||
|
||||
- 都没找到则抛出异常
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user