iOS设计模式 - KVC模式

KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。

KVC不需要调用明确的Get和Set方法。这样就可以在运行时动态在访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。

无论是Swift还是Objective-C,KVC的定义都是对NSObject的扩展来实现的,Objective-c中有个显式的NSKeyValueCoding类别名,而Swift没有,也不需要。

对于所有继承了NSObject在类型,都能使用KVC。一些纯Swift类和结构体是不支持KVC的。

下面是KVC最为重要的四个方法:

1
2
3
4
public func valueForKey(key: String) -> AnyObject? //获取键的值
public func setValue(value: AnyObject?, forKey key: String) //设置键的值
public func valueForKeyPath(keyPath: String) -> AnyObject? //获取具有路径的键的值
public func setValue(value: AnyObject?, forKeyPath keyPath: String) //设置具有路径的键的值

当然NSKeyValueCoding类别中还有其他的一些方法,下面列举一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class func accessInstanceVariablesDirectly() -> Bool
/**默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索。*/
public func validateValue(ioValue: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey inKey: String) throws
/**KVC提供属性值确认的API,它可以用来检查Set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。*/
public func mutableArrayValueForKey(key: String) -> NSMutableArray
/**这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。*/
public func valueForUndefinedKey(key: String) -> AnyObject?
/**如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。*/
public func setValue(value: AnyObject?, forUndefinedKey key: String)
/**和上一个方法一样,只不过是设值。*/
public func setNilValueForKey(key: String)
/**如果你在SetValue方法时将Value传nil,则会调用这个方法。*/
public func dictionaryWithValuesForKeys(keys: [String]) -> [String : AnyObject]
/**输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。*/
public func setValuesForKeysWithDictionary(keyedValues: [String : AnyObject])
/**通过这个方法直接可以将字典映射到对象。*/

上面的这些方法在碰到特殊情况或者有特殊需求还是会用到的,所以也是可以了解一下。后面的代码示例会有讲到其中的一些方法。

同时苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。建议有基础的或者英文好的开发者直接去看苹果的官方文档,相信你会对KVC的理解更上一个台阶。

KVC的实现原理

KVC是怎么使用的,我相信绝大多数的开发者都很清楚,我在这里就不再写简单的使用KVC来设值和取值的代码了,首先我们来探讨KVC在内部是按什么样的顺序来寻找key的。

存值 setValue

当调用setValue:属性值 forKey:@”name“的代码时,底层的执行机制如下:

  • 程序优先调用set:属性值方法,代码通过setter方法完成设置。注意,这里的是指成员变量名,首字母大写要符合KVC的全名规则,下同

    • 如果没有找到set<Key>:方法,KVC机制会检查:

      + (BOOL)accessInstanceVariablesDirectly

      方法有没有返回YES,默认该方法会返回YES;如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行:

      setValue: forUNdefinedKey :

      不过一般开发者不会这么做。

    • KVC机制会搜索该类里面有没有名为_<Key>的成员变量,无论该变量是在类接口部分定义,还是在类实现部分定义,也无论用了什么样的访问修饰符,只要存在以_<Key>命名的变量,KVC都可以对该成员变量赋值。

  • 如果该类即没有set<Key>:方法,也没有_<Key>成员变量,KVC机制会搜索_is<Key>的成员变量;

  • 和上面一样,如果该类即没有set:<Key>方法,也没有_<Key>_is<Key>成员变量,KVC机制再会继续搜索<Key>is<Key>的成员变量。再给它们赋值;

  • 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的:

    setValue:forUNdefinedKey:

    这个方法默认是抛出异常,当然你也可以对它进行重写;

  • 如果开发者想让这个类禁用KVC里,那么重写:

    + (BOOL)accessInstanceVariablesDirectly

    方法让其返回NO即可,这样的话如果KVC没有找到set<Key>:属性名时,会直接用:

    setValue:forUNdefinedKey:

Swift中没有发现accessInstanceVariablesDirectly中顺序搜索成员的特性!!!

下面我们使用代码来测试一下上述的机制,首先是:

搜索成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Dog: NSObject {
override class func accessInstanceVariablesDirectly() -> Bool {
print("\(#function)")
return false
}
override func valueForUndefinedKey(key: String) -> AnyObject? {
print("\(key)的值无法取出,因为没有找到\(key)这个属性")
return nil
}
override func setValue(value: AnyObject?, forUndefinedKey key: String) {
print("\(key)的值无法修改为\(value!),因为没有找到\(key)这个属性")
}
}
/**执行*/
let dog = Dog()
dog.setValue("newName", forKey: "name")
/**输出*/
accessInstanceVariablesDirectly()
accessInstanceVariablesDirectly()
name的值无法修改为newName,因为没有找到name这个属性

以上说明在setValue没有找到对应属性时,都会执行到accessInstanceVariablesDirectly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Dog: NSObject {
var _name: String!
func setName(name: String) {
print("\(#function)")
_name = name
}
func getName() -> String {
print("\(#function)")
return _name
}
override class func accessInstanceVariablesDirectly() -> Bool {
print("\(#function)")
return true
}
}
/**执行*/
let dog = Dog()
dog.setValue("newName", forKey: "name")
print(dog._name)
/**输出*/
setName
newName

以上说明具有对应的set<Key>方法时,即使不存在这个属性也可以通过编译。

成员不存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Dog: NSObject {
override func valueForUndefinedKey(key: String) -> AnyObject? {
print("\(key)的值无法取出,因为没有找到\(key)这个属性")
return nil
}
override func setValue(value: AnyObject?, forUndefinedKey key: String) {
print("\(key)的值无法修改为\(value!),因为没有找到\(key)这个属性")
}
}
/**执行*/
let dog = Dog()
dog.setValue("newName", forKey: "name")
let dogName = dog.valueForKey("toSetName")
print(dogName)
/**输出*/
name的值无法修改为newName,因为没有找到name这个属性
toSetName的值无法取出,因为没有找到toSetName这个属性
nil

以上说明当setValueForKeyvalueForKey中的Key无法找到时,会调用响应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Dog: NSObject {
var toSetName: String!
var name: String {
set(newValue) {
toSetName = newValue
}
get {
return toSetName
}
}
override class func accessInstanceVariablesDirectly() -> Bool {
print("\(#function)")
return true
}
override func valueForUndefinedKey(key: String) -> AnyObject? {
print("\(key)的值无法取出,因为没有找到\(key)这个属性")
return nil
}
override func setValue(value: AnyObject?, forUndefinedKey key: String) {
print("\(key)的值无法修改为\(value!),因为没有找到\(key)这个属性")
}
}
/**执行*/
let dog = Dog()
dog.setValue("newName", forKey: "name")
let dogName = dog.valueForKey("toSetName")
print(dogName)
/**输出*/
Optional(newName)

现在可以顺利的输出了。

取值 valueForKey

当调用ValueforKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:

  • 首先按get<Key> <key> is<Key>的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者int等值类型, 会做NSNumber转换;

  • 如果上面的getter没有找到,KVC则会查找:

    countOf<Key> & objectIn<Key>AtIndex & <Key>AtIndex

    这三种格式的方法。如果countOf<Key>和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合,它是NSKeyValueArray,是NSArray的子,这个代理集合将拥有以上方法的组合,还有一个可选的get<Key>: range:方法;

    所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。

  • 如果上面的方法没有找到,那么会查找:

    countOf<Key> & enumeratorOf<Key> & memberOf<Key>

    以上三种格式的方法,如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合,这个代理集合将拥有以上三种方法。

  • 如果还没有找到,再检查类方法:

    + (BOOL)accessInstanceVariablesDirectly

    如果返回YES(默认行为),那么和先前的设值一样,会按_Key,_isKey,Key,isKey的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱;

  • 如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用:

    valueForUndefinedKey:

  • 还没有找到的话,调用valueForUndefinedKey:

下面我们使用代码来验证以上的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class TwoTimesArray: NSObject {
var arrName: String!
var count: Int?
func incrementCount() {
self.count! += 1
}
func countOfNumbers() -> Int {
return self.count!
}
func objectInNumbersAtIndex(index: UInt) -> AnyObject {
return "\(index * 2)"
}
func getNum() -> Int {
return 10
}
func num() -> Int {
return 11
}
func isNum() -> Int {
return 12
}
}

以下的输出结果证明,在valueForKey方法执行时,如果对应的Key不存在,则会首先按get<Key> <key> is<Key>的顺序方法查找getter方法:

1
2
3
4
5
6
7
/**执行*/
let arr = TwoTimesArray()
let num = arr.valueForKey("num")
print(num)
/**输出*/
getNum()
Optional(10)

以下结果说明,当Key不存在,get<Key> <key> is<Key>方法也不存在时。如果countOf<Key>和:

objectIn<Key>AtIndex & <Key>AtIndex

两个方法中的一个被找到,那么就会返回该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**执行*/
let arr = TwoTimesArray()
let ar = arr.valueForKey("numbers")
for i in 0...5 {
print(ar![i])
}
/**输出*/
objectInNumbersAtIndex
0
objectInNumbersAtIndex
2
objectInNumbersAtIndex
4
objectInNumbersAtIndex
6
objectInNumbersAtIndex
8
objectInNumbersAtIndex
10

KVC中使用KeyPath

在开发过程中,一个类的成员变量有可能是其他的自定义类,你可以先用KVC获取出来再该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径KeyPath。

1
2
public func valueForKeyPath(keyPath: String) -> AnyObject? //获取具有路径的键的值
public func setValue(value: AnyObject?, forKeyPath keyPath: String) //设置具有路径的键的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Address: NSObject {
var country: String!
var city: String!
}
class People: NSObject {
var name: String!
var age: Int!
var address: Address!
override func valueForUndefinedKey(key: String) -> AnyObject? {
print("\(key)的值无法取出,因为没有找到\(key)这个属性")
return nil
}
}
/**执行*/
let people = People()
let peopleAdd = Address()
peopleAdd.country = "China"
people.address = peopleAdd
var china = people.address.country
var usa = people.valueForKey("address.country")
print("\(china) \(usa)")
people.setValue("USA", forKeyPath: "address.country")
china = people.address.country
usa = people.valueForKeyPath("address.country")
print("\(china) \(usa)")
/**输出*/
address.country的值无法取出,因为没有找到address.country这个属性
China nil
USA Optional(USA)

上面的代码简单在展示了KeyPath是怎么用的。如果使用了key而非KeyPath的话,KVC会直接查找address.country这个属性,很明显,这个属性并不存在,所以会再调用UndefinedKey相关方法。

而KVC对于KeyPath是搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。

KVC异常处理

KVC中最常见的异常就是不小心使用了错误的Key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。
通常在用KVC操作Model时,抛出异常的那两个方法是需要重写的。虽然一般很小出现传递了错误的Key值这种情况,但是如果不小心出现了,直接抛出异常让APP崩溃显然是不合理的。
一般在这里直接让这个Key打印出来即可,或者有些特殊情况需要特殊处理。
通常情况下,KVC不允许你要在调用setValue:属性值 forKey:@”name“(或者keyPath)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。

而在Swift中,无论是可选类型还是非可选类型,KVC都允许写入空值,但在使用时如果对非可选类型赋空值,程序将崩溃,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class People: NSObject {
var name: String!
var age: String?
var address: Address!
override func setNilValueForKey(key: String) {
print("无法更新\(key)的值,因为传入的是空值")
}
}
/**执行*/
let people = People()
people.setValue("20", forKey: "age")
print(people.age)
people.setValue(nil, forKey: "age")
print(people.age)
/**输出*/
Optional("20")
nil

如上所见,setNilValueForKey:在Swift中并不会执行。

KVC处理非对象和自定义对象

不是每一个方法都返回对象,但是valueForKey:总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开发者需要手动转换成原来的类型。

尽管valueForKey:会自动将值类型封装成对象,但是setValue:forKey:却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。

对于自定义对象,KVC也会正确的设值和取值。因为传递进去和取出来的都是id类型,所以需要开发者自己保证类型的正确性,运行时Objective-C在发送消息的会检查类型,如果错误会直接抛出异常。

1
2
3
4
5
6
7
8
9
10
/**执行*/
let people = People()
var peopleAdd = Address()
peopleAdd.country = "China"
people.setValue(peopleAdd, forKey: "address")
peopleAdd = people.valueForKey("address") as! Address
print(peopleAdd.country)
/**输出*/
China

KVC和字典

从NSDictionary取值的时候有两个方法:

objectForKey & valueForKey:

两种取值方法的区别

这两个方法具体有什么不同呢?先从NSDictionary文档中来看这两个方法的定义:

objectForKey: returns the value associated with aKey, or nil if no value is associated with aKey.

返回给定的key的value,若没有这个Key返回nil。

valueForKey: returns the value associated with a given key.

返回与给定的key的value。

直观上看这两个方法好像没有什么区别,但文档里valueForKey:有额外一点:

If key does not start with “@”, invokes objectForKey:.

一般来说key可以是任意字符串组合,如果key不是以@符号开头,这时候valueForKey: 等同于objectForKey:,

If key does start with “@”, strips the “@” and invokes [super valueForKey:] with the rest of the key.

如果是以@开头,去掉key里的@然后用剩下部分作为key执行[super valueForKey :]。

1
2
3
let dict = NSDictionary(object: "value", forKey: "key")
let a = dict.valueForKey("key")
let b = dict.objectForKey("key")

这时候a和b是一样的结果,因为key不是以@符号开头,这时候valueForKey:等同于objectForKey:,但如果是这样一个dict:

1
2
3
4
let dict = NSDictionary(object: "value", forKey: "key")
// 注意这个key是以@开头
let a = dict.valueForKey("@key")
let b = dict.objectForKey("@key")

b的取值为nil,a取值会直接crash掉,报错信息:

1
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSDictionaryI 0x7987d030> valueForUndefinedKey:]: this class is not key value coding-compliant for the key key.'

这是因为valueForKey:以@开头,去掉key里的@然后用剩下部分作为key执行[super valueForKey :],而super没有key这个属性,所以进入了valueForUndefinedKey方法中,抛出错误。

总结

objectForKey:是NSDictionary的方法,valueForKey:是KVC的方法, 两者都是键值对应,区别是valueForKey: 只允许使用NSString类型,objectForKey:可以是任意类型。

当对NSDictionary对象使用KVC时,valueForKey:的表现行为和objectForKey:一样。所以使用valueForKeyPath:用来访问多层嵌套的字典是比较方便的。KVC里面还有两个关于NSDictionary的方法:

1
2
3
4
public func dictionaryWithValuesForKeys(keys: [String]) -> [String : AnyObject]
/**是指输入一组key,返回这组key对应的属性,再组成一个字典。*/
public func setValuesForKeysWithDictionary(keyedValues: [String : AnyObject])
/**修改Model中对应key的属性。*/

下面直接用代码对这两个方法来进行演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**执行*/
let add = Address()
add.country = "China"
add.province = "GuangDong"
add.city = "ShenZhen"
add.district = "NanShan"
let arr = ["country","province","city","district"]
let dict = add.dictionaryWithValuesForKeys(arr)
print(dict)
let modifyDict: [String:String] = ["country":"USA","province":"california","city":"Los angle"]
add.setValuesForKeysWithDictionary(modifyDict)
print(add.dictionaryWithValuesForKeys(arr))
/**输出*/
["district": NanShan, "country": China, "city": ShenZhen, "province": GuangDong]
["district": NanShan, "country": USA, "city": Los angle, "province": california]

Demo下载请点击这里

参考链接

iOS开发技巧系列—详解KVC(我告诉你KVC的一切) - 黑暗中的孤影

KVC进阶(一)- 01_Jack

Objective-C KVC机制 - omegayy