利用Swift的反射机制遍历对象属性

前段时间写了一个macOS图床小应用cuImage,里面有些技术点一直没抽出时间总结和分享。临近毕业事情也比较多,只能挤时间了。本文将利用Swift的反射机制遍历对象属性,从而简化代码,提高代码复用率。

cuImage中每个图床都有相应的配置信息,如七牛云的图床配置信息QiniuHostInfo。我希望将其通过UserDefaults保存起来。(图床中有些敏感信息需要加密,但为了简化描述,本文就不涉及加密相关的点了,其实加密方面我也是小白菜。至于为什么不用Keychain加密就是另一回事了,这里略过。)

由于UserDefaults不支持自定义对象,如果强行直接保存,会在运行时抛出异常:

Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘Attempt to insert non-property list object ‘Abc’ for key ‘Xyz’.

为了让自定义对象也能够通过UserDefaults进行持久化存储,需要通过NSKeyedArchiver将自定义对象编码成NSData格式。这就需要遵循NSCoding协议,实现该协议声明的两个方法(init(_:)encode(_:))。一般的实现方式如下:

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
41
42
43
44
45
46
47
48
49
50
51
final class QiniuHostInfo: NSObject, NSCoding {
var name = ""
var accessKey = ""
var secretKey = ""
init(name: String = "", accessKey: String = "", secretKey: String = "") {
self.name = name
self.accessKey = accessKey
self.secretKey = secretKey
super.init()
}
init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: #keyPath(name)) as! String
accessKey = aDecoder.decodeObject(forKey: #keyPath(accessKey)) as! String
secretKey = aDecoder.decodeObject(forKey: #keyPath(secretKey)) as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: #keyPath(name))
aCoder.encode(accessKey, forKey: #keyPath(accessKey))
aCoder.encode(secretKey, forKey: #keyPath(secretKey))
}
}
final class ImgurHostInfo: NSObject, NSCoding {
var name = ""
var userName = ""
var password = ""
init(name: String = "", userName: String = "", password: String = "") {
self.name = name
self.userName = userName
self.password = password
super.init()
}
init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: #keyPath(name)) as! String
userName = aDecoder.decodeObject(forKey: #keyPath(userName)) as! String
password = aDecoder.decodeObject(forKey: #keyPath(password)) as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: #keyPath(name))
aCoder.encode(userName, forKey: #keyPath(userName))
aCoder.encode(password, forKey: #keyPath(password))
}
}

这种方式的缺点在于,如果QiniuHostInfo增加新的属性或者重构代码时修改属性,需要在NSCoding声明的两个方法中都添加或修改对应的代码,还可能有疏漏。而且,当需要新增一个图床(如ImgurHostInfo)时,又得按相同的的步骤撸一遍代码。

下面是我最终采用的方式。首先,我为各种图床信息类写了个公共基类HostInfo,将实现NSCoding协议方法的任务交给了基类HostInfo,一次性搞定。以后每增加一个图床,只需要在相应的图床信息子类(如QiniuHostInfo, ImgurHostInfo)定义好自身的属性就行了,这样子类的代码就清爽多了。

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
41
42
43
44
45
class HostInfo: NSObject, NSCoding {
var name = ""
override init() {
super.init()
}
convenience required init?(coder aDecoder: NSCoder) {
self.init()
forEachChildOfMirror(reflecting: self) { key in
setValue(aDecoder.decodeObject(forKey: key), forKey: key)
}
}
func encode(with aCoder: NSCoder) {
forEachChildOfMirror(reflecting: self) { key in
aCoder.encode(value(forKey: key), forKey: key)
}
}
func forEachChildOfMirror(reflecting subject: Any, handler: (String) -> Void) {
var mirror: Mirror? = Mirror(reflecting: subject)
while mirror != nil {
for child in mirror!.children {
if let key = child.label {
handler(key)
}
}
// Get super class's properties.
mirror = mirror!.superclassMirror
}
}
}
final class QiniuHostInfo: HostInfo {
var accessKey = ""
var secretKey = ""
}
final class ImgurHostInfo: HostInfo {
var userName = ""
var password = ""
}

上面的代码中,init(_:)encode(_:)都需要用到反射,所以我将公共部分抽出来,并把每访问到一个属性时要做的操作以闭包的形式传给了forEachChildOfMirror(_:_:)函数。其中用到了superclassMirror是因为在父类中可能也有一些公共属性需要被遍历到,如HostInfo中的name属性。于是遍历完当前类的属性后,就可以通过superclassMirror顺着继承链往上爬,访问到继承连上的类的所有属性了。

如果读者们有其他不错的实现方式,期待你们的分享。

0%