前言
本文翻译自Swift & the Objective-C Runtime
翻译的不对的地方还请多多包涵指正,谢谢~
Swift和Objective-C运行时
即使不写一行OC(Objective-C)代码,每个Swfit应用还是执行在OC运行时内部,打开一个动态调度的世界和有关的运行时操作。的确,项目只用Swift框架的情况不总是存在,但一旦这种情况来临,可能就会出现运行时内只存在Swift而没有OC。只要OC运行时一直存在,咱们就该发挥它的全部潜力。
我们将采取一种新的,侧重于Swift的角度看看两个运行时技术:关联对象及函数置换,NSHipster
在开发历史只有OC语言的时候对这两种技术已有介绍。
注:文章主要涉及两种技术在Swift的使用,如想详细了解请点击上述链接。
关联对象(Associated Objects)
Swift扩展对于在现有Cocoa类中添加功能函数有极大地灵活性,但就像Swift同胞OC的类别(category
)一样,扩展是有限制的。也就是说,通过扩展也不能对现有的类添加属性。
令人欣慰的是,OC关联对象可实现对现有类添加属性。例如,为了在工程内对所有View Controllers
添加名为descriptiveName
属性,我们只需简单地在属性的get
及set
方法中使用objc_get/setAssociatedObject()
方法添加属性。
extension UIViewController {
private struct AssociatedKeys {
static var DescriptiveName = "nsh_DescriptiveName"
}
var descriptiveName: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.DescriptiveName) as? String
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.DescriptiveName,
newValue as NSString?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
}
注意使用私有嵌套的结构体内的
static var
变量,这样做创建了需要的一个静态的关联对象键值而且不会污染全局命名空间。
方法置换(Method Swizzling)
有时为了方便,有时为了解决框架的bug,有时因为没有其他方法,需要改变已存在类的方法的实现。方法置换可以交换两个方法的实现,最重要的是覆盖已存在的方法的同时不改变原方法的实现。
在本例中,通过改变UIViewController’s viewWillAppear
方法的实现在任何界面即将出现时打印一条信息。置换发生在类的静态initialize
方法,替换的实现是在nsh_viewWillAppear
方法中:
extension UIViewController {
public override class func initialize() {
struct Static {
static var token: dispatch_once_t = 0
}
// make sure this isn't a subclass
if self !== UIViewController.self {
return
}
dispatch_once(&Static.token) {
let originalSelector = Selector("viewWillAppear:")
let swizzledSelector = Selector("nsh_viewWillAppear:")
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
// MARK: - Method Swizzling
func nsh_viewWillAppear(animated: Bool) {
self.nsh_viewWillAppear(animated)
if let name = self.descriptiveName {
print("viewWillAppear: \(name)")
} else {
print("viewWillAppear: \(self)")
}
}
}
load vs initialize (Swift版本)
OC运行时在加载和初始化应用类过程中通常会自动调用两个类方法:load
和initialize
。在文章method swizzling
中,Mattt指出从安全和便利角度,替换过程通常应该在load
方法内。load
方法每个类只会调用一次,且是在加载类时被调用。从另外方面,initialize
方法能被类及其子类(对于UIViewController
来说很可能存在)调用,但在没有任何消息发送到该类情况下,initialize
不会被调用。
不幸的是,Swift在运行时不会调用load
方法,所以Mattt推荐的方式不能实现。作为替代,我们选择了次优方法:
- <strong>在
initialize
方法中做置换</strong>
这种实现是安全的,只要你在运行时检查好类型且用dispatch_once
包裹置换方法。 - <strong>在
app delegate
中做置换</strong>
不通过类扩展添加置换方法,而简单的把替换过程在application(_:didFinishLaunchingWithOptions:)
中执行。取决于你修改的类,这种方式可能是有效的且应该能保证你的的代码每次都执行。
后记
记住修改OC运行时应该是最后的手段而不是第一想到的方法。修改代码所基于的框架或第三方对于应用来说是不是很稳定。谨慎对待~