Swift学习笔记-动态特性

我们知道 Objective-C 的最显著的特性就是强大的运行时能力,这使得 Objective-C 几乎是完全灵活的一本语言。强大的自省,反射,运行时的消息派发,KVC,KVO 都让这个有着几十年历史的语言依然焕发着生机。Swift 作为一门现代化的编程语言,是十分安全的语言。绝大多数的内容都是在编译期就已经决定了的。这使得 Swift 的运行时灵活性有所折扣。但需求总是千变万化的,运行时特性往往能起到四两拨千斤的强大作用,所以 Swift 还是引入了一些动态特性,我们来逐一看一下。

获取对象类型

在 Objective-C 中我们可以很容易地做到这件事,只要使用 -class 方法就可以轻松地获取到到对象的类,使用 NSStringFromClass 方法还可以将类的名称转换成字符串。

Swift 中分为两种情况来考虑,第一种情况是 NSObject 的子类,另一种情况是纯 Swift class

NSObject 子类

对于 NSObject 子类来讲,其实它们类的信息的存储方式和在 Obj-C 中并没有什么变化,只是没有了 -class 方法供你直接使用了而已。所以这时候可以使用 Objective-C 运行时(runtime)来获取类的信息:

1
2
let name = object_getClass(NSDate())
print(name) //__NSDate

这里使用的其实是 Objective-C runtime 中的方法 object_getClass 这个方法接受一个 AnyObject! 参数,并返回 AnyClass! 类型。也就是说这个方法可以接受 nil 作为参数并返回一个 nil。

纯 Swift class

这里需要使用 dynamicType 来获取类型,值得一提的是在 Swift 1.2 版本之后,我们就也可以对 NSObject 子类使用 dynamicType 来获取类型了。这样的话,在 Swift 中获取对象类型的方法就变得统一起来。

Selector

在 Objective-C 中我们可以通过 selector 做很多事情,包括询问对象是否接受某个方法,指定通知调用的方法,设定 target-action 等等都是由 selector 来完成的。在 Obj-C 中我们可以很方便的使用 @selector 创建 selector 或者 NSSelectorFromString 来根据字符串动态创建 selector。Swift 中不再提供 @selector 我们只能够通过字符串来创建 Selector。

原来的 SEL 在 Swift 中对应的是一个 Selector 结构体,它提供了只接受一个字符串的初始化方法。

1
let selector = Selector("test")

Swift 中的 Selector 实现了 StringLiteralCovertible 协议,所以我们可以直接使用字符串赋值来完成创建 Selector 的工作。

1
self.performSelector("test")

注意:

在 Swift 中,如果 selector 对应的方法是被 private 修饰的话(也就是说这个方法仅在 Swift 中可见),在运行时会寻找不到对应的方法,报出unrecongnized selector错误。这时需要在 private 前面加上 @objc 关键字来修饰。这样在运行时就可以找到相应的方法了。

1
2
3
4
@objc private func test()
{
print("test")
}

当第一个参数有外部变量的话,需要在方法名和第一个外部参数之间加上 with:

1
2
3
4
5
6
func test(external name:String)
{
print("test")
}

self.performSelector("testWithExternal:", withObject: "")

动态创建对象

Objective-C 可以直接使用字符串或者 Class 来创建对象实例,但是 Swift 中尚未发现有这样的方法。

反射

反射是一种在运行时检测,访问或者修改类型的的行为的特性。在 Objective-C 中一般很少提及“反射”这个词,更多的是“运行时”(runtime)这种说法。这是因为 Objective-C 能够提供的运行时特性比一般的反射要强大得多。对于NSObject 子类来讲,使用 Swift 的时候也可以无缝地使用这些特性。如果抛开这些 Objective-C 的特性的话,纯 Swift 虽然也提供了一些反射的内容,但是相对要弱得多。以下内容均是基于 Swift2.2 环境:

Swift 中定义的类型都实现了 _Reflectable 接口,这个接口提供了一个 getMirror 方法用于获取实现了 MirrorType 接口的镜像,这个镜像对象包含了类内部基本的信息。开发者需要通过 reflect 方法来获取这个对象。进而在不了解对象类型的情况下对其内部进行操作。

_reflect 可以作用于所有的类型,下面举一个例子:

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 superapple : NSObject {
var name:String
init (name: String)
{
self.name = name
}
}


class apple : superapple
{
var color:String

init (color:String)
{
self.color = color
print(self.color)
super.init(name:"")
}

convenience init(name: String, color: String)
{
self.init(color : color)
self.name = name
}
}

var app = apple.init(name: "apple", color: "red")
let mirt = _reflect(app)
print("属性个数\(mirt.count)")
print("属性1:\(mirt[0].0) 值:\(mirt[0].1.summary)")
print("属性2:\(mirt[1].0) 值:\(mirt[1].1.summary)")

//输出:
//属性个数2
//属性1:super 值:superapple
//属性2:color 值:red

我们发现 MirrorType 类型的对象中包含了所有的属性,我们可以通过下标来访问这些属性,每一个下标对应着一个元组(Tuple),每个元组的第一位表示属性的名称,第二位是封装了属性的值的一个 Mirror 类型,值需要通过 summary 或者 MirrorType 的 value 来访问。

现时的 Swift 版本中反射功能仍然很弱,我们只可以读取属性的值,但是并不能设置属性的值。希望之后的版本可以提供类似 KVC 的强大反射特性。

自省(Introspection)

Swift 的类型绝大多数都是在编译期就已经确定的,但是实际中有时候还是需要在运行时来判断对象属于什么类型,这时可以使用 is 运算符来进行判断,is 相当于 Objective-C 中的 isKindOfClass 方法,用于判断一个对象是否属于一个类或者他的子类。不同点在于,is 不但可以用于 class 类型,也可以用于 struct ,enum 甚至基本类型的判断。用法如下:

1
2
3
4
5
6
7
8
9
class Test {
var name = "skyfly"
}
var instance: AnyObject = Test()

if instance is Test
{
print("instance is Test's Instance")
}

是就返回 true,不是就会返回 false。
但是注意的一点是,编译器会检查类型判定的必要性,没有必要的判定会被编译器发出警告。比如如下代码:

1
2
3
4
5
6
7
8
9
10
class Test {
var name = "skyfly"
}
var instance = Test()

if instance is Test
{
print("instance is Test's Instance")
}
// warning: is Test is always true

KVC

纯 Swift 中并没有提供 KVC 的功能,不过所有继承自 NSObject 的子类还是可以直接使用 KVC 的,因为 NSObject 有 NSKeyValueCoding 扩展。使用方法也大同小异,基本上只是语法变形而已。

KVO

同 KVC 一样,纯 Swift 并没有提供 KVO 的功能,使用 KVO 仍然是基于 NSObject 子类对象的,实质上 KVO 和 KVC 依然是基于 Objective-C 运行时实现的,这一点也无可非议。在 Swift 中使用 KVO 需要将 观察的属性使用 dynamic 来修饰。

Swift 中使用 KVO 面临的阻碍较多,我们只能观测被 dynamic 修饰的属性,而往往第三方框架中的属性没有这个修饰,或者我们无法修改使用的源码,这时就需要继承这个类并且使用 dynamic 重写想要观察的属性。

对于纯 Swift 类型来讲,由于实现原理的问题,暂时没有对其实现 KVO 的办法。目前只能使用属性观察来做替代了。

1
2
3
4
5
6
class TestForKVC:NSObject{
var name = "skyfly"
}
var instance = TestForKVC()
var value = instance.valueForKey("name") as! String
instance.setValue("hello world",forKey:"name")

方法调配(Method Swizzing)

对于 NSObject 子类,使用方法和 Objective-C 中基本类似,只是对语法做一些变形。对于纯 Swift 类型来讲,如果想要使用 Swizzle 就需要在原来的方法和替换的方法前面都加上 dynamic 标记以声明他们使用动态派发机制。

虽然 Swift 的 Method Swizzing 完全依赖了 Objective-C 运行时,但是仍然有一个的第三方实现了完全不借助 Objective-C 运行时来实现了 Method Swizzing,详见 SWRoute,他利用了一些内存的 hack 手段,利用 Swift 封装过的类似函数指针的东西来实现了方法调配。

关联对象(Associated Object)

Swift 中仍然可以使用关联对象。仍然是对原有方法进行语法变形即可。