前言
本文翻譯自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運行時應該是最后的手段而不是第一想到的方法。修改代碼所基于的框架或第三方對于應用來說是不是很穩定。謹慎對待~