Swift3——Swift3中的Runtime

记得学习OC的时候直到第二年才开始接触到Runtime,之前一直都是基础,UI,项目的简单开发,当我真正接触Runtime的时候,有种相见恨晚的感觉,之后再项目中也有经常使用到相关技术,当然15年开始Runtime在面试中也变得非常重要了……

Swift3 Runtime

纯Swift类没有动态性,但在方法、属性前添加dynamic修饰可以获得动态性。

Runtime是一套比较底层的纯C语言的API,属于C语言库, 包含了很多底层的C语言API。在我们平时编写的iOS代码中, 最终都是转成了runtime的C语言代码。

所谓运行时,也就是在编译时是不存在的,只是在运行过程中才去确定对象的类型、方法等。利用Runtime机制可以在程序运行时动态修改类、对象中的所有属性、方法等。

还记得我们在网络请求数据处理时,调用了-setValuesForKeysWithDictionary:方法来设置模型的值。这里什么原理呢?为什么能这么做?其实就是通过Runtime机制来完成的,内部会遍历模型类的所有属性名,然后设置与key对应的属性名的值。

我们在使用运行时的地方,都需要包含头文件:#import

<objc/runtime.h>。如果是Swift就不需要包含头文件,就可以直接使用了。

Swift中如何使用runtime

Swift代码中已经没有了Objective-C的运行时消息机制, 在代码编译时即确定了其实际调用的方法. 所以纯粹的Swift类和对象没有办法使用runtime, 更不存在method swizzling.

为了兼容Objective-C, 凡是继承NSObject的类都会保留其动态性, 依然遵循Objective-C的运行时消息机制, 因此可以通过runtime获取其属性和方法, 实现method swizzling.

获取对象所有属性名

利用运行时获取对象的所有属性名是可以的,但是变量名获取就得用另外的方法了。我们可以通过class_copyPropertyList方法获取所有的属性名称。

对于Swift版,使用C语言的指针就不容易了,因为Swift希望尽可能减少C语言的指针的直接使用,因此在Swift中已经提供了相应的结构体封装了C语言的指针。但是看起来好复杂,使用起来好麻烦。看看Swift版的获取类的属性名称如何做:

class Person: NSObject {
var name: String = ""
var hasBMW = false

override init() {
super.init()
}

func allProperties() ->[String] {
// 这个类型可以使用CUnsignedInt,对应Swift中的UInt32
var count: UInt32 = 0

let properties = class_copyPropertyList(Person.self, &count)

var propertyNames: [String] = []

// Swift中类型是严格检查的,必须转换成同一类型
for var i = 0; i < Int(count); ++i {
// UnsafeMutablePointer<objc_property_t>是
// 可变指针,因此properties就是类似数组一样,可以
// 通过下标获取
let property = properties[i]
let name = property_getName(property)

// 这里还得转换成字符串
let strName = String.fromCString(name);
propertyNames.append(strName!);
}

// 不要忘记释放内存,否则C语言的指针很容易成野指针的
free(properties)

return propertyNames;
}
}

测试一下是否获取正确:

let p = Person()
p.name = "Lili"

// 打印结果:["name", "hasBMW"],说明成功
p.allProperties()

获取对象的所有属性名和属性值

对于获取对象的所有属性名,在上面的-allProperties方法已经可以拿到了,但是并没有处理获取属性值,下面的方法就是可以获取属性名和属性值,将属性名作为key,属性值作为value

func allPropertyNamesAndValues() ->[String: AnyObject] {
var count: UInt32 = 0
let properties = class_copyPropertyList(Person.self, &count)

var resultDict: [String: AnyObject] = [:]
for var i = 0; i < Int(count); ++i {
let property = properties[i]

// 取得属性名
let name = property_getName(property)
if let propertyName = String.fromCString(name) {
// 取得属性值
if let propertyValue = self.valueForKey(propertyName) {
resultDict[propertyName] = propertyValue
}
}
}

return resultDict
}

测试一下:

let dict = p.allPropertyNamesAndValues()
for (propertyName, propertyValue) in dict.enumerate() {
print("propertyName: (propertyName), propertyValue: (propertyValue)")
}

打印结果与上面的一样,由于array属性的值为nil,因此不会处理。

propertyName: 0, propertyValue: (“name”, Lili)

获取对象的所有方法名

通过class_copyMethodList方法就可以获取所有的方法。

func allMethods() {
var count: UInt32 = 0
let methods = class_copyMethodList(Person.self, &count)

for var i = 0; i < Int(count); ++i {
let method = methods[i]
let sel = method_getName(method)
let methodName = sel_getName(sel)
let argument = method_getNumberOfArguments(method)

print("name: (methodName), arguemtns: (argument)")
}
}

测试一下调用:

p.allMethods()

获取对象的成员变量名称

要获取对象的成员变量,可以通过class_copyIvarList方法来获取,通过ivar_getName来获取成员变量的名称。对于属性,会自动生成一个成员变量。

Swift的成员变量名与属性名是一样的,不会生成下划线的成员变量名,这一点与Oc是有区别的。

func allMemberVariables() ->[String] {
var count:UInt32 = 0
let ivars = class_copyIvarList(Person.self, &count)

var result: [String] = []
for var i = 0; i < Int(count); ++i {
let ivar = ivars[i]

let name = ivar_getName(ivar)

if let varName = String.fromCString(name) {
result.append(varName)
}
}

return result
}

测试一下:

let array = p.allMemberVariables()
for varName in array {
print(varName)
}

打印结果,说明Swift的属性不会自动加下划线,属性名就是变量名:

name
array

运行时发消息

iOS中,可以在运行时发送消息,让接收消息者执行对应的动作。可以使用objc_msgSend方法,发送消息。

很抱歉,似乎在Swift中已经没有这种写法了。如果有,请告诉我。

Category扩展”属性”

iOS的category是不能扩展存储属性的,但是我们可以通过运行时关联来扩展“属性”。

Swift版的要想扩展闭包,就比OC版的要复杂得多了。这里只是例子,写了一个简单的存储属性扩展。

let s_HYBFullnameKey = "s_HYBFullnameKey"

extension Person {
var fullName: String? {
get { return objc_getAssociatedObject(self, s_HYBFullnameKey) as? String }
set {
objc_setAssociatedObject(self, s_HYBFullnameKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}

Objective-C Category 可以随意重写本类的方法, Swift的Extension虽然易用, 但仍然没有Category那样方便的重写方法.

Swizzle还是可以在Extension替换掉本类的任意方法. (Swift修改CocoaPods管理的第三库福音) 目前Swift3对于这个并不友好.

Swift调用方法的时候是直接访问内存, 而不是在运行时查找地址, 意味着普通的方法, 你需要在方法前加dynamic修饰字, 告诉编译器跳过优化而是转发. 否则你是拦截不到方法.
(注:viewDidLoad等方法不用加daynamic也可以截取到方法)

class Swizzler {
dynamic func originalFunction() -> String {
return "Original function"
}

dynamic func swizzledFunction() -> String {
return "Swizzled function"
}
}

let swizzler = Swizzler()

print(swizzler.originalFunction()) // prints: "Original function"
let classToModify = Swizzler.self

let originalMethod = class_getInstanceMethod(classToModify, #selector(Swizzler.originalFunction))
let swizzledMethod = class_getInstanceMethod(classToModify, #selector(Swizzler.swizzledFunction))
method_exchangeImplementations(originalMethod, swizzledMethod)

print(swizzler.originalFunction())  // prints: "Swizzled function"

在开发中,我们比较常用的是使用关联属性的方式来扩展我们的“属性”,以便在开发中简单代码。我们在开发中使用关联属性扩展所有响应事件、将代理转换成block版等。比如,我们可以将所有继承于UIControl的控件,都拥有block版的点击响应,那么我们就可以给UIControl扩展一个TouchUp、TouchDown、TouchOut的block等。

对于动态获取属性的名称、属性值使用较多的地方一般是在使用第三方库中,比如MJExtension等。这些三方库都是通过这种方式将Model转换成字典,或者将字典转换成Model。

这里, 要注意Swift的代码与Objective-C代码的语法区别.

同时, 对于一般OC代码的method swizzling, 在load方法中执行即可. 而Swift没有load, 所以要在initialize中执行.

使用方式:

btn.cs_accpetEventInterval = 1.0

Swift中的@objc和dynamic关键字

继承自NSObject的类都遵循runtime, 那么纯粹的Swift类呢?

在属性和方法之前加上@objc关键字, 则一般情况下可以在runtime中使用了. 但有一些情况下, Swift会做静态优化而无法使用runtime.

要想完全使得属性和方法被动态调用, 必须使用dynamic关键字. 而dynamic关键字会隐式地加上@objc来修饰.

获取Swift类的runtime信息的方法, 要加上Swift模块名:

id cls = objc_getClass("DemoSwift.MySwiftClass")
坚持原创技术分享,您的支持将鼓励我继续创作!