Swift Runtime

我们都知道 Objective-C 是一门动态的语言,有的时候我们会使用 Runtime 处理一些在 Objective-C 上面无法实现或者很难实现的功能。例如:在扩展中添加属性;动态的获取属性的名称,方法名等。那么究竟什么是 Runtime

什么是 Runtime

Runtime 简称运行时。Objective-C 就是运行时机制,也就是在程序运行时候的一些机制,其中最主要的是消息机制。对于我们熟悉的C语言,函数的调用在编译的时候会决定调用哪个函数。但对于 Objective-C 的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

也就有了下面这两点结论:

  1. 在编译阶段,Objective-C 可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
  2. 在编译阶段,C语言调用未实现的函数就会报错。

Swift 中的 Runtime


好了上面说了这么多都是在说 Objective-C 的 Runtime,那么 Swift 的 Runtime 是什么样的呢?我们来写几行代码看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Demo {
var name: String?
var address: String?

func myPrint() {
print("Demo")
}
}

var count: UInt32 = 0
let demo = Demo()
let list = class_copyPropertyList(object_getClass(demo), &count)

for idx in 0..<numericCast(count) {
if let item = list?[idx],
let name = String(utf8String: property_getName(item)) {

print("Property: \(name)")
}
}

猜一下,会输出什么?当然,这里什么都不输出!为什么?因为我们创建的类 Demo 是一个纯 Swift 的类,因为 Swift 是一门静态语言,所以我们对其运用 Runtime 机制当然是不可能获取到 Demo 类的属性的。如果我非要在 Swift 代码中运用 Runtime 技术呢?当然是有办法的。我们可以在你要获取的属性(函数同理)前用 @objc 修饰即可(当然加上 dynamic 也是可以的,不过编译器会提示错误,必须要在 dynamic 前添加 @objc),就像如下这样:

1
2
3
4
5
6
7
8
class Demo {
@objc var name: String?
@objc dynamic var address: String?

@objc func myPrint() {
print("Demo")
}
}

我们在添加如下的代码来打印出方法名:

1
2
3
4
5
6
7
8
var fCount: UInt32 = 0
let funcList = class_copyMethodList(object_getClass(demo), &fCount)
for idx in 0..<numericCast(fCount) {
if let item = funcList?[idx] {
let name = NSStringFromSelector(method_getName(item))
print("Method: \(name)")
}
}

输出如下:

1
2
3
4
5
6
7
Property: name
Property: address
Method: address
Method: name
Method: setName:
Method: setAddress:
Method: myPrint

为什么加上 @objc 就有 Runtime 机制了呢?Swift 不是静态语言吗?

我们来看一下官方文档里对 @objc 是怎么说的。

把这个特性用到任何可以在 Objective-C 中表示的声明上——例如,非内嵌类,协议,非泛型枚举(原始值类型只能是整数),类和协议的属性、方法(包括 settergetter ),初始化> > 器,反初始化器,下标。 objc 特性告诉编译器,这个声明在 Objective-C 代码中是可用的。

给扩展应用这个特性与为这个扩展中所有不显式标记为 nonobjc 特性的成员应用是一样的效果。

objc 特性标记的类必须继承自一个 Objective-C 中定义的类。如果你把 objc 用到类或协议中,它会隐式地应用于该类或协议中 Objective-C 兼容的成员上。如果一个类继承自另一个> 带 objc 特性标记或 Objective-C 中定义的类,编译器也会隐式地给这个类添加 objc 特性。标记为 objc 特性的协议不能继承自非 objc 特性的协议。

objc 特性同样会在下面的情况中隐式地添加:

  • 声明是子类的重写,并且父类的声明有 objc 特性;
  • 声明满足的需求来自一个拥有 objc 特性的协议;
  • 声明有 IBAction , IBOutlet , IBDesignable , IBInspectable , NSManaged, 或者 GKInspectable 特性。

如果你在一个枚举中使用 objc 特性,枚举名和每个成员名串联起来,作为枚举成员暴露给 Objective-C 代码。成员名首字母大写。例如,一个 Swift Planet 枚举成员叫做 venus ,> 它作为一个叫 PlanetVenus 的成员暴露到 Objective-C 代码中。

objc 特性可以接受一个特性实参,由一个标识符组成。当你想在 Objective-C 中为 objc 特性标记的实体暴露一个不同的名字时,用这个特性。你可以把这个实参用在命名类,枚举,枚举成> 员,协议,方法,getter,setter,初始化器。下面的例子把 ExampleClass 中 enabled 属性的getter作为 isEnabled 暴露给 Objective-C 代码,而不仅仅是属性本身的名字。

1
2
3
4
5
6
7
8
@objc
class ExampleClass: NSObject {
var enabled: Bool {
@objc(isEnabled) get {
// Return the appropriate value
}
}
}

首先有 @objc 这个关键字,它是用来将 Swift 的 API 暴漏给 Objective-C 和 Runtime 使用的,文档里也很清楚的说明了,如果你类继承自 Objective-C 的类,这个标识符就会被自动加进去,加了这标识符的属性、方法无法保证都会被运行时调用,因为 Swift 会做静态优化,想要完全被声明成动态调用,必须使用 dynamic 标识符修饰,当然添加了 dynamic 的时候,它会自己在加上 @objc 这个标识符。

举个例子:

1
2
3
4
5
6
7
8
class Demo: NSObject {
var dBool = true
var dInt = 1

func demoTest() {
print("NSObject Class")
}
}

虽然这时我们没有在 dBooldInt 两个属性添加 @objc 修饰,但是 Runtime 时依然能够获取到,因为类继承了 Objective-C 中的 NSObject,他会隐式的在属相前面添加 @objc,同理,继承自 UIViewController 等的类,都有这个特性。