iOS --- 如何在Swift專案中使用runtime?
在Objective-C的專案中, 經常遇到通過runtime來獲取類和物件的成員變數, 屬性, 方法, 在此基礎上可以實現method swizzling.
關於runtime的相關內容, 請參考部落格:
iOS — 理解Runtime機制及其使用場景
iOS—防止UIButton重複點選的三種實現方式
iOS — 使用runtime解決3D Touch導致UIImagePicker崩潰的問題
JSPatch即使用JavaScriptCore.framework, 使用JS程式碼呼叫任何OC的原生介面, 通過runtime來替換任意OC的原生方法, 以此來實現實時地修復線上bug.
Swift中如何使用runtime
Swift程式碼中已經沒有了Objective-C的執行時訊息機制, 在程式碼編譯時即確定了其實際呼叫的方法. 所以純粹的Swift類和物件沒有辦法使用runtime, 更不存在method swizzling.
為了相容Objective-C, 凡是繼承NSObject的類都會保留其動態性, 依然遵循Objective-C的執行時訊息機制, 因此可以通過runtime獲取其屬性和方法, 實現method swizzling.
請看如下的程式碼:
//
// UIButton+CSExtension.swift
// CSSwiftExtension
//
// Created by Chris Hu on 16/6/20.
// Copyright © 2016年 icetime17. All rights reserved.
//
import UIKit
// MARK: - UIButton Related
public extension UIButton {
private struct cs_associatedKeys {
static var accpetEventInterval = "cs_acceptEventInterval"
static var acceptEventTime = "cs_acceptEventTime"
}
// 重複點選的間隔
var cs_accpetEventInterval: NSTimeInterval {
get {
if let accpetEventInterval = objc_getAssociatedObject(self, &cs_associatedKeys.accpetEventInterval) as? NSTimeInterval {
return accpetEventInterval
}
return 1.0
}
set {
objc_setAssociatedObject(self, &cs_associatedKeys.accpetEventInterval, newValue as NSTimeInterval, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var cs_acceptEventTime: NSTimeInterval {
get {
if let acceptEventTime = objc_getAssociatedObject(self, &cs_associatedKeys.acceptEventTime) as? NSTimeInterval {
return acceptEventTime
}
return 1.0
}
set {
objc_setAssociatedObject(self, &cs_associatedKeys.acceptEventTime, newValue as NSTimeInterval, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
override public class func initialize() {
let before: Method = class_getInstanceMethod(self, #selector(UIButton.sendAction(_:to:forEvent:)))
let after: Method = class_getInstanceMethod(self, #selector(UIButton.cs_sendAction(_:to:forEvent:)))
method_exchangeImplementations(before, after)
}
func cs_sendAction(action: Selector, to target: AnyObject?, forEvent event: UIEvent?) {
if NSDate().timeIntervalSince1970 - self.cs_acceptEventTime < self.cs_accpetEventInterval {
return
}
if self.cs_accpetEventInterval > 0 {
self.cs_acceptEventTime = NSDate().timeIntervalSince1970
}
self.cs_sendAction(action, to: target, forEvent: event)
}
}
以上, 即通過runtime的方式解決UIButton的重複點選問題.
UIButton繼承自NSObject, 因此遵循runtime. 事實上, 對於基本框架如Foundation, UIKit等, 都可以使用runtime.
這裡, 要注意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")
關於Demo
本文的Demo請參考CSSwiftExtension.
這是是一個Swift的extension集合, 包含了一些常見的方法.