键值编码是由
NSKeyValueCoding
非正式协议启用的一种机制,对象采用该协议来提供对其属性的间接访问。当对象符合键值编码时,其属性可以通过简洁、统一的消息传递接口通过字符串参数进行寻址。这种间接访问机制补充了实例变量及其关联访问器方法提供的直接访问。
您通常使用访问器方法来访问对象的属性。get访问器(或getter)返回属性的值。集合访问器(或设置器)设置属性的值。在Objective-C中,您还可以直接访问属性的基础实例变量。以上述任何一种方式访问对象属性都很简单,但需要调用特定于属性的方法或变量名称。随着属性列表的增长或变化,访问这些属性的代码也必须增长或变化。相比之下,符合键值编码的对象提供了一个简单的消息传递界面,该界面在其所有属性中都一致。KVC是一个基本概念,是许多其他可可技术的基础,如KVC、可可绑定、核心数据和AppleScript可性。在某些情况下,键值编码也有助于简化代码。
实现机制(摘自苹果文档,下面有干货非纯文档)
valueforkey
valueForKey:
的默认实现,给定一个key
参数作为输入,执行以下过程,从接收valueForKey:
调用的类实例中操作。
按顺序在实例中搜索第一个访问器方法,名称如
get<Key>
、<key>
、is<Key>
或_<key>
。如果找到,请调用它,然后继续第5步并附上结果。否则,请继续下一步。-
如果没有找到简单的访问器方法,请在实例中搜索名称与模式
countOf<Key>
和objectIn<Key>AtIndex:
(对应NSArray
类定义的原始方法)和<key>AtIndexes:
(对应NSArray方法
objectsAtIndexes:)匹配的方法。如果找到其中第一个和至少两个中的一个,请创建一个响应所有
NSArray
方法的集合代理对象并返回该对象。否则,请继续第3步。代理对象随后将其收到的任何
NSArray
消息转换为countOf<Key>
、objectIn<Key>AtIndex:
和<key>AtIndexes:
消息的某种组合,转换为创建它的键值编码兼容对象。如果原始对象还实现了名为get<Key>:range:
的可选方法,则代理对象也会在适当时使用该方法。实际上,代理对象与键值编码兼容对象一起工作,允许基础属性的行为就像NSArray
一样,即使它不是。 -
If no simple accessor method or group of array access methods is found, look for a triple of methods named
countOf<Key>
,enumeratorOf<Key>
, andmemberOf<Key>:
(corresponding to the primitive methods defined by the NSSet class).如果找到所有三种方法,请创建一个响应所有
NSSet
方法的集合代理对象并返回该对象。否则,请继续第4步。此代理对象随后将其接收的任何
NSSet
消息转换为countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>:
消息的某种组合到创建它的对象。实际上,代理对象与键值编码兼容对象一起工作,允许基础属性的行为就像NSSet
一样,即使它不是。 如果没有找到简单的访问器方法或集合访问方法组,并且接收器的类方法accessInstanceVariablesDirectly返回
YES
,请按此顺序搜索名为_<key>
、_is<Key>
、<key>
或is<Key>
的实例变量。如果找到,请直接获取实例变量的值,然后转到第5步。否则,请继续第6步。如果检索到的属性值是对象指针,只需返回结果。 如果该值是
NSNumber
支持的标量类型,请将其存储在NSNumber
实例中并返回。
如果结果是NSNumber不支持的标量类型,请转换为NSValue
对象并返回该对象。如果所有其他方法都失败了,请调用valueForUndefinedKey:。默认情况下,这会引发异常,但
NSObject
的子类可能会提供特定于密钥的行为。
setValueForKey
按顺序查找第一个名为
set<Key>:
或_set<Key>
的访问器。如果找到,请使用输入值(或根据需要打开的值)调用它,然后完成。如果没有找到简单的访问器,并且类方法
accessInstanceVariablesDirectly
返回YES
,请按顺序查找名称为_<key>
、_is<Key>
、<key>
或is<Key>
的实例变量。如果找到,请直接使用输入值(或未包装的值)设置变量并完成。找不到访问器或实例变量时,调用
setValue:forUndefinedKey:
。默认情况下,这会引发异常,但NSObject
的子类可能会提供特定于密钥的行为。
Array/Set
mutableOrderedSetValueForKey/mutableSetValueForKey:的默认实现:识别与valueForKey相同的简单访问器方法和有序集访问器方法:,并遵循相同的直接实例变量访问策略,但总是返回可变集合代理对象,而不是valueForKey:返回的不可变集合。此外,它还执行以下操作:
- 搜索名称如下的方法
insertObject:in<Key>AtIndex:
andremoveObjectFrom<Key>AtIndex:
(对应于 NSMutableOrderedSetclass), 而且
insert<Key>:atIndexes:和
remove<Key>AtIndexes:`(对应 insertObjects:atIndexes: 和 removeObjectsAtIndexes:).
如果找到至少一种插入方法和至少一种删除方法,则返回的代理对象将发送以下方法的组合: insertObject:in<Key>AtIndex:
, removeObjectFrom<Key>AtIndex:
, insert<Key>:atIndexes:
, and remove<Key>AtIndexes:
发送给原始收件人的消息mutableOrderedSetValueForKey:
收到“NSMutableOrderedSet”消息时显示消息。
代理对象还使用名称为replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
等名称的方法,当它们存在于原始对象中时。
如果没有找到可变集方法,搜索名称为set<Key>:的访问器方法。在这种情况下,返回的代理对象每次收到NSMutableOrderedSet消息时都会向原始接收器发送set<Key>:消息。
如果既没有找到可变集消息也没有找到访问器,并且接收器的accessInstanceVariablesDirectly类方法返回
YES
,请按照此顺序搜索名称为_<key>
或<key>
的实例变量。如果找到此类实例变量,返回的代理对象会将其收到的任何NSMutableOrderedSet
消息转发到实例变量的值,该值通常是NSMutableOrderedSet
或其子类之一的实例。如果所有其他操作都失败,则返回的代理对象将发送
setValue:forUndefinedKey:
发送给原始收件人的消息mutableOrderedSetValueForKey:
每当它收到可变集合消息时。
setValue:forUndefinedKey:
的默认实现会引发NSUndefinedKeyException,但对象可能会覆盖此行为。
自定义实现KVC
因为苹果内部闭源所以根据原理猜测他的实现方式
- (BOOL)performSelectorWithMethodName:(NSString *)aSelector value:(id)anArgument {
SEL func = NSSelectorFromString(aSelector);
if ([self respondsToSelector:func]) {
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
[self performSelector:func withObject:anArgument];
_Pragma("clang diagnostic pop")
return YES;
}
return NO;
}
-(void)jl_setValue:(nullable id)value forKey:(NSString *)key {
//key非空判断
if (!key || key.length == 0) {
return;
}
//找到相关方法 set<Kety> _set<Key> setIs<Key>
//Key要大写
NSString * Key = key.capitalizedString;
NSString * setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString * _setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString * setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
if ([self performSelectorWithMethodName:setKey value:value]) {
return;
}
if ([self performSelectorWithMethodName:_setKey value:value]) {
return;
}
if ([self performSelectorWithMethodName:setIsKey value:value]) {
return;
}
//判断当前是否能否直接复制示例变量
if ([self.class accessInstanceVariablesDirectly]) {
@throw [NSException exceptionWithName:@"JLUnknownKeyException" reason:[NSString stringWithFormat:@"***[%@ valueForUndefineKey:]: this class is not key value codeing-comliant for the key name.****",self] userInfo:nil];
return;
}
//4. 找相关示例变量进行赋值
//4.1定一个收集实例变量的可变数组
NSMutableArray * mArray = [self getIvarListName];
//_<key> _is<Key> <key> is<Key>
NSString * _Key = [NSString stringWithFormat:@"_%@:",Key];
NSString * _isKey = [NSString stringWithFormat:@"_is%@:",Key];
NSString * isKey = [NSString stringWithFormat:@"is%@:",Key];
NSArray * keyarr = @[_Key,_isKey,key,isKey];
for (NSString * ikey in keyarr) {
if ([mArray containsObject:ikey]) {
Ivar ivar = class_getInstanceVariable([self class], ikey.UTF8String);
object_setIvar(self, ivar, value);
return;
}
}
@throw [NSException exceptionWithName:@"JLUnknownKeyException" reason:[NSString stringWithFormat:@"***[%@ valueForUndefineKey:]: this class is not key value codeing-comliant for the key name.****",self] userInfo:nil];
}
-(nullable id)jl_valueforKey:(NSString *)key {
// 1:刷选key 判断非空
if (key == nil || key.length == 0) {
return nil;
}
// 2:找到相关方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex
// key 要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
return nil;
}
上面代码是根据kvc文档简单的猜测了他内部实现了原理。但是他还有个进阶用法,其实上面的原理上面也写到了
请参考下列代码
class Person: NSObject {
var penArr:[String] = []
}
Person *p = [Person new];
p.penArr = [NSMutableArray arrayWithObjects:@"pen0", @"pen1", @"pen2", @"pen3", nil];
NSArray *arr = [p valueForKey:@"pens"]; // 动态成员变量
NSLog(@"pens = %@", arr);
//NSLog(@"%@",arr[0]);
NSLog(@"%d",[arr containsObject:@"pen9"]);
正常情况下因为没有pens这个属性调用肯定会崩溃,但是如果进行一步骚操作就不会崩了这个操作就是实现对应的
// 个数
- (NSUInteger)countOfPens {
return [self.penArr count];
}
//// 获取值
- (id) objectInPensAtIndex:(NSUInteger)index {
return [NSString stringWithFormat:@"pens %lu", index];
}
// 是否包含这个成员对象
- (id)memberOfPens:(id)object {
return [self.penArr containsObject:object] ? object : nil;
}
// 迭代器
- (id)enumeratorOfPens {
// objectEnumerator
return [self.penArr reverseObjectEnumerator];
}
将该key映射到原来的消息体中。这样就实现了kvc的骚操作。
不得不说看文档还是牛逼啊,以后要多看文档可以发现许多牛逼的东西
KVO
当我们对A类添加监听的时候,系统会自动生成一个NSKVONotifying_A的子类,这个类重写了A的class、superclass、deealloc方法和该属性的Set方法,同时A类的对象的isa指针指向了该虚拟子类。当监听属性改变的时候系统调用NSSetobjectValueandNotify,这个方法的执行流程是(willchangeValueforkey->改变父类的值->didchangeValueforkey->observeValueForKey:ofObject:change:context:),如果设置*automaticallyNotifiesObserversForKey:(NSString )key为NO的时候则需要手动触发KVO即手动调用willchangeValueforkey和didchangeValueforkey.
func _NSSetObjectValueAndNotify {
...
willchangeValueforkey
...
"
objc_msgSendSupper '改变父类的值(猜测这样实现)
"
...
didchangeValueforkey
...
observeValueForKey:ofObject:change:context:
}
Q1: 为什么重写系统的class/superclass、deealloc方法
因为该子类为系统自动生成苹果想伪装成并没有这个类 所以重写class/superclass ,但是调用 objc_getClass()这个方法时候依然会暴露,因为这个方法是调用调用对象的isa指针指向。dealloc则是系统还有一些其他的事情处理
自定义KVO
自定义KVO需要遵守KVO的几个基本原理,下面方法只是简单实现了一个kvo,存在context判断以及多个observer都存在的时候处理。这个都需要加判断
1.重写他的set方法所以当调用
addObserver
的时候需要判断当前类有没有实现set<KeyPath>
方法
2.生成NSKVONotifying_XX的派生类
3. 重写父类的class方法,setKeyPath,dealloc方法
#pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *leaveString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length-4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
NSString * oldName = NSStringFromClass([self class]);
//动态生成子类
NSString * newClassName = [NSString stringWithFormat:@"%@%@",kJLKVOPrefix,oldName];
Class newClass = NSClassFromString(newClassName);
if (newClass) {///判断类是否已经被注册
return newClass;
}
/**
* 如果内存不存在,创建生成
* 参数一: 父类
* 参数二: 新类的名字
* 参数三: 新类的开辟的额外空间
*/
// 2.1 : 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2 : 注册类
objc_registerClassPair(newClass);
// 2.3.1 : 添加class : class的指向是父类
SEL classSel = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSel);
const char * classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSel, (IMP)jl_class, classTypes);
// 2.3.2 : 添加setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)jl_setter, setterTypes);
return newClass;
}
- (void)jl_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
//1.验证是否存在示例方法不让示例进来
[self judgeSetterMethodFromKeyPath:keyPath];
// 2.动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3. isa的指向 : KVONotifying_Person
object_setClass(self, newClass);
// 4: 保存观察者
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJLKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)jl_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
// 指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
static void jl_setter(id self,SEL _cmd,id newValue){
NSLog(@"来了:%@",newValue);
// 4: 消息转发 : 转发给父类
// 改变父类的值 --- 可以强制类型转换
void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// void /* struct objc_super *super, SEL op, ... */
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
//objc_msgSendSuper(&superStruct,_cmd,newValue)
lg_msgSendSuper(&superStruct,_cmd,newValue);
// 既然观察到了,下一步不就是回调 -- 让我们的观察者调用
// - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
// 1: 拿到观察者
id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJLKVOAssiociateKey));
// 2: 消息发送给观察者
SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
// objc_msgSend(observer,observerSEL);
objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
// [self observeValueForKeyPath:keyPath ofObject:nil change:@{keyPath:newValue} context:nil];
// objc_msgSend(self);
}