Runtime之KVC实现探究

我们都知道iOS开发中有很多 黑魔法 ,KVC就是其中之一,在平时开发中我们也会使用KVC去获取一些系统未公开的API方法,但同时我们可能要承担一些被拒或者因系统API改变导致的问题。这篇文章我们从源码的角度分析KVC的实现。

KVC使用示例

- (void)kvcValueForKeyTest {
self.p = [Person new];
self.p.name = @"LeeWong";
self.p.age = 30;
NSLog(@"Person age = %@",self.p.name);
NSLog(@"Person name = %@",@(self.p.age));
}
- (void)kvcSetValueForKey {
self.p = [Person new];
[self.p setValue:@"LeeWong" forKey:@"name"];
[self.p setValue:@(30) forKey:@"age"];
NSLog(@"Person age = %@",[self.p valueForKey:@"age"]);
NSLog(@"Person name = %@",[self.p valueForKey:@"name"]);
}

打印结果:

2020-12-19 18:52:54.058580+0800 Runtime-KVC[99046:16759483] Person age = 30
2020-12-19 18:52:54.058700+0800 Runtime-KVC[99046:16759483] Person name = LeeWong
2020-12-19 18:52:54.058784+0800 Runtime-KVC[99046:16759483] -----------------------------
2020-12-19 18:52:54.058882+0800 Runtime-KVC[99046:16759483] Person age = LeeWong
2020-12-19 18:52:54.058990+0800 Runtime-KVC[99046:16759483] Person name = 30

这两种情况是我们平时应用最广泛的KVC(Key-Value-Coding)的方法,通常用来获取属性值或者给对象的某个属性值赋值,即使对象的属性是私有的只要我们可以确认他的key值,我们就可以获取或者设置值。

KVC原理

在查看KVC相关方法时,我们发现其主要的方法都集中在 NSKeyValueCoding.h 这个文件中,下面我们来看下这个文件中的方法:

@property (class, readonly) BOOL accessInstanceVariablesDirectly;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
@end
@interface NSArray<ObjectType>(NSKeyValueCoding)
- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
@end
@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)
- (nullable ObjectType)valueForKey:(NSString *)key;
@end
@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)
- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;
@end
@interface NSOrderedSet<ObjectType>(NSKeyValueCoding)
- (id)valueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)setValue:(nullable id)value forKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
@end
@interface NSSet<ObjectType>(NSKeyValueCoding)
- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
@end

这个文件中主要包含了

NSObject(NSKeyValueCoding) ,

NSArray<ObjectType>(NSKeyValueCoding) ,

NSDictionary<KeyType,ObjectType>(NSKeyValueCoding) ,

NSMutableDictionary<KeyType,ObjectType>(NSKeyValueCoding) ,

NSOrderedSet<ObjectType>(NSKeyValueCoding) ,

NSSet<ObjectType>(NSKeyValueCoding)

主要是常用的NSObject以及集合类型 NSArray , NSDictionary , NSMutableDictionary , NSOrderedSet , NSSet 的KVC相关方法分类。从这个分类中的分组我们也将本篇文章要讲述的内容分为两部分NSOject和集合类型的属性。

KVC实现猜想

我们在前面一篇文章中提到了KVO的实现,系统实际上继承我们要监听的类创建了一个子类,然后在子类中重写对应属性的 setter 方法,然后在设置值方法的前后调用了 willChangeValueForKeydidChangeValueForKey 的方法通知外部的监听者。

那么我们首先看下KVC获取到的对象的类型有没有改变:

- (void)kvoObjectClassChangeTest {
self.father = [Father new];
Son *son1 = [Son new];
Son *son2 = [Son new];
self.father.name = @"LeeWong";
self.father.children = @[son1,son2];
NSLog(@"Father 的children 类型是%@",[[self.father valueForKey:@"children"] class]);
NSLog(@"Father 的children 类型是%@",object_getClass([self.father valueForKey:@"children"]));
NSLog(@"Father 的name 类型是 %@",[[self.father valueForKey:@"name"] class]);
NSLog(@"Father 的name 类型是 %@",object_getClass([self.father valueForKey:@"name"]));
}

打印结果:

2020-12-19 20:40:29.671973+0800 Runtime-KVC[1432:16837560] Father 的children 类型是__NSArrayI
2020-12-19 20:40:29.672078+0800 Runtime-KVC[1432:16837560] Father 的children 类型是__NSArrayI
2020-12-19 20:40:29.672189+0800 Runtime-KVC[1432:16837560] Father 的name 类型是 __NSCFConstantString
2020-12-19 20:40:29.672306+0800 Runtime-KVC[1432:16837560] Father 的name 类型是 __NSCFConstantString

这里我们看出KVC并没有给我们要获取的属性或者类动态添加子类或者其他属性,那么到底是怎么实现的呢?

否定了刚才的想法后,我们知道Runtime是可以动态获取到对象的所有属性的,那是否意味着当我们去给对象的属性通过setValue:forKey方法设值的时候是动态的获取这个对象的属性列表,如果属性列表中包含要设置的key那么久调用这个key对应的设置方法实现的呢?

下面我们通过这段代码验证下:

@synthesize name = _name;
- (void)setName:(NSString *)name {
_name = name;
NSLog(@"Person setName %@",name);
}
- (NSString *)name {
NSLog(@"Person getname %@",_name);
return _name;
}
- (void)kvoSetMethodCallTest {
self.father = [Father new];
[self.father setValue:@"LeeWong" forKey:@"name"];
NSLog(@"kvoSetMethodCallTest father name %@",self.father.name);
}

我们调用 kvoSetMethodCallTest 方法后控制台输出结果如下:

2020-12-19 20:53:05.265737+0800 Runtime-KVC[1701:16847901] Person setName LeeWong
2020-12-19 20:53:05.265856+0800 Runtime-KVC[1701:16847901] Person getname LeeWong
2020-12-19 20:53:05.265937+0800 Runtime-KVC[1701:16847901] kvoSetMethodCallTest father name LeeWong

通过上述输出我们看到实际上无论是 setValue:forKey 还是 valueForKey: 最终都是调用到了属性的setter和getter方法。

那么我们的猜想基本得到验证了,那么我们下面就来看下是如何调用到getter和setter方法的呢?

KVC的实现

在了解KVC实现之前我们首先需要了解下KVC相关方法:

setValueForKey

/* Given a value and a key that identifies an attribute, set the value of the attribute. Given an object and a key that identifies a to-one relationship, relate the object to the receiver, unrelating the previously related object if there was one. Given a collection object and a key that identifies a to-many relationship, relate the objects contained in the collection to the receiver, unrelating previously related objects if there were any.
The default implementation of this method does the following:
1. Searches the class of the receiver for an accessor method whose name matches the pattern -set<Key>:. If such a method is found the type of its parameter is checked. If the parameter type is not an object pointer type but the value is nil -setNilValueForKey: is invoked. The default implementation of -setNilValueForKey: raises an NSInvalidArgumentException, but you can override it in your application. Otherwise, if the type of the method's parameter is an object pointer type the method is simply invoked with the value as the argument. If the type of the method's parameter is some other type the inverse of the NSNumber/NSValue conversion done by -valueForKey: is performed before the method is invoked.
2. Otherwise (no accessor method is found), if the receiver's class' +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _<key>, _is<Key>, <key>, or is<Key>, in that order. If such an instance variable is found and its type is an object pointer type the value is retained and the result is set in the instance variable, after the instance variable's old value is first released. If the instance variable's type is some other type its value is set after the same sort of conversion from NSNumber or NSValue as in step 1.
3. Otherwise (no accessor method or instance variable is found), invokes -setValue:forUndefinedKey:. The default implementation of -setValue:forUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application.
Compatibility notes:
- For backward binary compatibility with -takeValue:forKey:'s behavior, a method whose name matches the pattern -_set<Key>: is also recognized in step 1. KVC accessor methods whose names start with underscores were deprecated as of Mac OS 10.3 though.
- For backward binary compatibility, -unableToSetNilForKey: will be invoked instead of -setNilValueForKey: in step 1, if the implementation of -unableToSetNilForKey: in the receiver's class is not NSObject's.
- The behavior described in step 2 is different from -takeValue:forKey:'s, in which the instance variable search order is <key>, _<key>.
- For backward binary compatibility with -takeValue:forKey:'s behavior, -handleTakeValue:forUnboundKey: will be invoked instead of -setValue:forUndefinedKey: in step 3, if the implementation of -handleTakeValue:forUnboundKey: in the receiver's class is not NSObject's.
*/
- (void)setValue:(nullable id)value forKey:(NSString *)key;

实际上上面的方法注释中已经明确告知我们方法的实现了:

  • 给定一个值和一个标识属性的键,请设置属性的值;
  • 给定一个对象和一个标识一对一关系的key,将该对象与接收者相关联,如果存在则取消先前相关的对象的关联;
  • 给定一个集合对象和一个标识多对多关系的key,将包含在集合中的对象与接收者相关联,如果存在则取消先前相关的对象的关联;

方法的实现步骤如下:

  • 搜索该类以及父类中名称符合-set

    :格式的方法,如果找到,检查其参数类型。如果参数类型不是对象类型且参数为nil,则调用-setNilValueForKey:方法,这个方法的默认实现是抛出NSInvalidArgumentException异常,不过你可以重写该方法自行实现。如果参数类型为对象类型,set

    :方法会被直接调用,这个参数也会被直接使用。如果参数能被转化为NSNumber/NSValue类型,参数会在存取器方法被调用之前进行转换。

  • 如果方法没有被找到,如果对象的+accessInstanceVariablesDirectly属性返回的是YES,那么按照_

    , _is

    ,

    , is

    的顺序搜索该类的实例变量。如果找到这个实例变量,当其为对象类型时,该实例变量会在旧值释放之后被设置新值。当其为其他类型时,那么按照步骤1中的类型转换规则设置这个实例变量的值。

  • 如果上述两步都失败了,方法和实例变量都没有被找到,-setValue:forUndefinedKey:方法将会被调用。这个方法的默认实现是抛出NSUndefinedKeyException异常,不过你可以重写该方法自行实现。

下面我们来验证下API所说:

属性设置为nil

Father.m
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"Father setNilValueForKey %@",key);
}
ViewController.m
- (void)setValueForKeyTest {
self.father = [Father new];
[self.father setValue:nil forKey:@"name"];
[self.father setValue:nil forKey:@"age"];
}

控制台输出结果为:

2020-12-19 21:42:29.523439+0800 Runtime-KVC[2388:16881195] Person setName (null)
2020-12-19 21:42:29.523581+0800 Runtime-KVC[2388:16881195] Father setNilValueForKey age

这验证了第一步中说的查找set

方法,如果找到了name且未对象类型那么直接调用setName:方法,value会被直接使用,如果找到了setAge方法,但参数不是对象类型且值为nil,那么直接调用setNilValueForKey方法(我们实现了这个方法在内部做了输出操作)。

查找对应的成员变量

@interface Person ()
{
NSString *_isName1;
NSInteger _isAge1;
}
+ (BOOL)accessInstanceVariablesDirectly {
NSLog(@"Father accessInstanceVariablesDirectly");
return YES;
}
- (NSString *)getisNameValue {
return _isName1;
}
- (NSInteger)getisAgeValue {
return _isAge1;
}
ViewController.m
- (void)setValueForKeyTest {
self.father = [Father new];
[self.father setValue:@"LeeWong" forKey:@"name1"];
NSLog(@"setValueForKeyTest getisNameValue %@",[self.father getisNameValue]);
[self.father setValue:@(10) forKey:@"age1"];
NSLog(@"setValueForKeyTest getisAgeValue %@",@([self.father getisAgeValue]));
}

控制台打印结果为:

2020-12-19 21:54:26.320358+0800 Runtime-KVC[2685:16893209] Father accessInstanceVariablesDirectly
2020-12-19 21:54:26.320474+0800 Runtime-KVC[2685:16893209] setValueForKeyTest getisNameValue LeeWong
2020-12-19 21:54:26.320589+0800 Runtime-KVC[2685:16893209] Father accessInstanceVariablesDirectly
2020-12-19 21:54:26.320705+0800 Runtime-KVC[2685:16893209] setValueForKeyTest getisAgeValue 10

这个结果验证了我们setValue:forKey:的的第二步,搜索当前类中是否存在 _<key>, _is<Key>, <key>, or is<Key> (示例中我们是存在_age1/_name1)的实例变量。

未找到对应的属性

Person.m
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"Person setValue %@ forUndefinedKey %@",value,key);
}
ViewController.m
- (void)setValueForUndefineKeyTest {
self.father = [Father new];
[self.father setValue:@"LeeWong" forKey:@"name1"];
[self.father setValue:@(10) forKey:@"age1"];
}

控制台打印结果:

2020-12-19 22:01:02.601408+0800 Runtime-KVC[2814:16898946] Person setValue LeeWong forUndefinedKey name1
2020-12-19 22:01:02.601546+0800 Runtime-KVC[2814:16898946] Person setValue 10 forUndefinedKey age1

综合上面几步,我们总结了下setValue:forKey:的调用流程为:

未完待续

leewong
我还没有学会写个人说明!
上一篇

ClickHouse MaterializeMySQL实时同步MySQL汇总

下一篇

Raft算法系列教程1:Leader选举

你也可能喜欢

评论已经被关闭。

插入图片