docs: refine

This commit is contained in:
LiuBinPeng
2022-04-19 17:15:49 +08:00
parent e2871d54e4
commit 7241220c8e
92 changed files with 10837 additions and 1963 deletions

View File

@@ -1,8 +1,4 @@
# KVC && KVO
# KVC && KVO
## 一、基本用法-字典快速赋值
@@ -24,13 +20,12 @@ KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setVal
运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null
- 情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值
运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分
```
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
```
@@ -38,7 +33,6 @@ KVC 可以将字典里面和 model 同名的 property 进行快速赋值 `setVal
我们照样可以利用 **setValue:forUndefinedKey:** 去处理
```objective-c
//model
@property (nonatomic,copy)NSString *name;
@@ -65,7 +59,6 @@ NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
```
- 情况五: 注册依赖键.
KVO 可以观察属性的二级属性对象的所有属性变化。说人话就是“假如 Person 类有个 Dog 类Dog 类有 name、fur、weight 等属性,我们给 Person 的 Dog 属性观察,假如 Dog 的任何属性变化是Person 的观察者对象都可以拿到当前的变化值。我们只需要在 Person 中写下面的方法即可”
@@ -82,7 +75,7 @@ self.person.dog.weight = 50;
// Person.m
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
NSArray *affectingKeys = @[@"name", @"fur", @"weight"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
@@ -91,9 +84,6 @@ self.person.dog.weight = 50;
}
```
## 二、 KVO 的本质
kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础。
@@ -109,18 +99,39 @@ kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础
return NO;
}
```
3. 若类有实例变量 NSString *_foo 调用 setValue:forKey: 是以 foo 还是 _foo 作为 key
都可以
4. KVC 的 keyPath 中的集合运算符如何使用
- 必须用在 **集合对象** 或者 **普通对象的集合属性** 上
-简单的集合运算符有 @avg、@count、@max、@min、@sum
5. KVO 和 KVC 的 keyPath 一定是属性吗?
可以是成员变量
可以是成员变量
6. KVO 中 派生类的 setter 方法内部实现调用了 Foundation 框架中的 `_NSSetIntValueAndNotify`.
7. 直接修改对象的成员变量会触发 KVO 吗?
不会。因为成员变量没有 setter.
```
@interface Person: NSObject
{
@public:
    int age;
}
@end
```
8. 手动触发 KVO调用 willChangeValueForKey、didChangeValueForKey
9.
@@ -142,17 +153,13 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
- 键值观察通知依赖于 NSObject 的两个方法:`willChangeValueForKey:、didChangeValueForKey:` 。在一个被观察属性改变之前,调用 `willChangeValueForKey:` 记录旧的值。在属性值改变之后调用 `didChangeValueForKey:`,从而 `observeValueForKey:ofObject:change:context:` 也会被调用。
![KVO原理图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2018_11_12_KVO.png)
为什么要选择是继承的子类而不是分类呢?
子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给程序员补救,如果未做处理则奔溃
关于分类与子类的关系可以看看我之前的 [文章](1.50.md).
## 四、 模拟实现系统的 KVO
1. 创建被观察对象的子类
@@ -160,6 +167,7 @@ Apple 文档告诉我们:被观察对象的 `isa指针` 会指向一个中间
3. 外界改变 isa 指针class方法重写
我们用自己的类模拟系统的 KVO。
```
//NSObject+LBPKVO.h
#import <Foundation/Foundation.h>
@@ -169,7 +177,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface NSObject (LBPKVO)
- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
@@ -189,15 +197,15 @@ NS_ASSUME_NONNULL_END
Class myclass = objc_allocateClassPair(self.class, [currentClassName UTF8String], 0);
// 生成后不能马上使用,必须先注册
objc_registerClassPair(myclass);
//2. 重写 setter 方法
class_addMethod(myclass,@selector(setName:) , (IMP)setName, "v@:@");
//3. 修改 isa
object_setClass(self, myclass);
//4. 将观察者保存到当前对象里面
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN);
//5. 将传递的上下文绑定到当前对象里面
objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN);
}
@@ -212,7 +220,7 @@ void setName (id self, SEL _cmd, NSString *name) {
object_setClass(self, class_getSuperclass(class));
//2. 调用父类的 setName 方法
objc_msgSend(self, @selector(setName:), name);
//3. 调用观察
id observer = objc_getAssociatedObject(self, "observer");
id context = objc_getAssociatedObject(self, "context");
@@ -235,7 +243,7 @@ void setName (id self, SEL _cmd, NSString *name) {
_person.hobbies = [@[@"iOS"] mutableCopy];
NSDictionary *context = @{@"name": @"成吉思汗", @"hobby" : @"弯弓射大雕"};
[_person lbpKVO_addObserver:self forKeyPath:@"hobbies" options:(NSKeyValueObservingOptionNew) context:(__bridge void * _Nullable)(context)];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
@@ -247,10 +255,8 @@ void setName (id self, SEL _cmd, NSString *name) {
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
```
KVO 的缺陷:
KVO 虽然很强大,你只能重写 `-observeValueForKeyPath:ofObject:change:context: ` 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block 没门儿。感觉如果加入 block 就更棒了。
@@ -261,4 +267,54 @@ KVO 的改装:
## 五、 KVC
`setValueForKey` 用来设置对象的一层属性值修改。
`setValueForKeyPath` 可以设置对象的某个属性(属性本身是对象)的属性值修改
KVC 之后会触发 KVO。为什么探究下 `setValueForKey`
`[self.person setValue:@10 forKey:@"age"]` 会先调用 `setKey:` 同名的方法,找不到则调用 `_setKey:` 的方法,如果还是找不到则调用 `+(BOOL)accessInstanceVariableDirectlt`,如果该方法返回 YES则可以直接修改成员变量的值会按照 _key、_isKey、key、isKey 的顺序寻找成员变量,如果找到则直接赋值,没找到则抛出异常 NSUnknownKeyException
整个流程如下
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/KVC-process.png)
```
@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 的顺序访问成员变量。找到哪个则返回值
- 都没找到则抛出异常
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/KVC-get-process.png)