Swift和Objective-C運行時

前言

本文翻譯自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屬性,我們只需簡單地在屬性的getset方法中使用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運行時在加載和初始化應用類過程中通常會自動調用兩個類方法:loadinitialize。在文章method swizzling中,Mattt指出從安全和便利角度,替換過程通常應該在load方法內。load方法每個類只會調用一次,且是在加載類時被調用。從另外方面,initialize方法能被類及其子類(對于UIViewController來說很可能存在)調用,但在沒有任何消息發送到該類情況下,initialize不會被調用。

不幸的是,Swift在運行時不會調用load方法,所以Mattt推薦的方式不能實現。作為替代,我們選擇了次優方法:

  • <strong>在initialize方法中做置換</strong>
    這種實現是安全的,只要你在運行時檢查好類型且用dispatch_once包裹置換方法。
  • <strong>在app delegate中做置換</strong>
    不通過類擴展添加置換方法,而簡單的把替換過程在application(_:didFinishLaunchingWithOptions:)中執行。取決于你修改的類,這種方式可能是有效的且應該能保證你的的代碼每次都執行。

后記

記住修改OC運行時應該是最后的手段而不是第一想到的方法。修改代碼所基于的框架或第三方對于應用來說是不是很穩定。謹慎對待~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容