你肯定也想過
在OC中相信每一個iOS開發都知道Runtime, 現在Swift也更新到4.0版本了,要是你也學習過Swift的話你可能也會想過這樣一個問題,OC大家都知道是有動態性的,你能通過Runtime 的API獲取你想要的屬性方法等等,那Swift呢?是不是也和OC一樣呢?
這個問題在我看Swift的時候也有想過,帶着這個問題就總結出了今天這篇文章。
先說說這個Runtime,在自己之前的文章中有總結過關於OC的Runtime以及它API的一些基本的方法和在項目中具體的使用,在這里再大概的提一下Runtime的基本的概念:
RunTime簡稱運行時。OC就是運行時機制,也就是在程序運行時候的一些機制,其中最主要的是消息機制。對於我們熟悉的C語言,函數的調用在編譯的時候會決定調用哪個函數。但對於OC的函數,屬於動態調用過程,在編譯的時候並不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。
也就有了下面這兩點結論:
1、在編譯階段,OC可以調用任何函數,即使這個函數並未實現,只要聲明過就不會報錯。
2、在編譯階段,C語言調用未實現的函數就會報錯。
看看Swift Runtime
先不直接丟出結論,從下面的簡單的代碼入手,一步步的找出我們想要的答案:
我們定義一個純Swift的類 TestASwiftClass ,代碼如下:
class TestASwiftClass{
var aBoll :Bool = true
var aInt : Int = 0
func testReturnVoidWithaId(aId : UIView) {
print("TestASwiftClass.testReturnVoidWithaId")
}
}
代碼也是很簡單,我們定義了兩個變量一個方法,下面我們再寫一個繼承自 UIViewController 的 ViewController ,代碼如下:
class ViewController: UIViewController{
let testStringOne = "testStringOne"
let testStringTwo = "testStringTwo"
let testStringThr = "testStringThr"
var count:UInt32 = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let SwiftClass = TestASwiftClass()
let proList = class_copyPropertyList(object_getClass(SwiftClass),&count)
for i in 0..<numericCast(count) {
let property = property_getName(proList?[i]);
print("屬性成員屬性:%@",String.init(utf8String: property!) ?? "沒有找到你要的屬性");
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
上面的代碼也很簡單,我們在ViewController中添加了一些變量,然后通過Runtime的方法嘗試着先來獲取一下我們最上面定義的純Swift類TestASwiftClass的屬性,你運行上面代碼你就會發現:
什么都沒有!!!為什么??
下面我們先給出答案,用它來解釋一下為什么我們通過上面Runtime的API沒有獲取到任何東西,然后再接着用OC來證明一下我們說的結論:
C 語言是在函數編譯的時候決定調用那個函數,在編譯階段,C要是調用了沒有實現的函數就會報錯。
OC 的函數是屬於動態調用,在編譯的時候是不能決定真正去調用那個函數的,只有在運行的時候才能決定去調用哪一個函數 ,在編譯階段,OC可以調用任何的函數,即使這個函數沒有實現,只要聲明過也就不會報錯。
Swift 純Swift類的函數的調用已經不是OC的運行時發送消息,和C類似,在編譯階段就確定了調用哪一個函數,所以純Swift的類我們是沒辦法通過運行時去獲取到它的屬性和方法的。
Swift 對於繼承自OC的類,為了兼容OC,凡是繼承與OC的都是保留了它的特性的,所以可以使用Runtime獲取到它的屬性和方法等等其他我們在OC中獲得的東西。
針對上面給出的結論,我們看看Swift對於繼承自OC的類是不是保留了OC所有的特性呢?再看下面代碼,只是做一個簡單的修改,把通過object_getClass方法獲取的對象寫成self:
let proList = class_copyPropertyList(object_getClass(self),&count)
for i in 0..<numericCast(count) {
let property = property_getName(proList?[i]);
print("屬性成員屬性:%@",String.init(utf8String: property!) ?? "沒有找到你要的屬性");
}
通過上面的方法我們獲取到的日志如下:

可以看到我們獲取到了我們在ViewController中定義的變量。這樣也就證明了的確是上面答案說的那樣。
那這樣就又衍生出一個問題
那Swiftw就沒辦法利用Runtime了嗎?
想一想,要是真的Swift沒辦法利用Runtime,那是一件得多讓人失望的事!答案也肯定是否定的,我們還是能讓Swift用Runtime的。看下面的代碼:
class TestASwiftClass{
dynamic var aBoll :Bool = true
var aInt : Int = 0
dynamic func testReturnVoidWithaId(aId : UIView) {
print("TestASwiftClass.testReturnVoidWithaId")
}
}
上面還是我們定義的 TestASwiftClass 類,不同的地方不知道大家注意到沒?
嗯,我們利用了dynamic(英文單詞動態的意思)關鍵字,在第一個變量和方法的定義前面我們添加了這個關鍵字,那添加了這個關鍵字之后又什么變化呢?我們再通過最開始我們獲取純Swift類的代碼獲取一下試試,看結果!

結果:
可以看到這里是獲取到了變量了的。(這里是獲取屬性沒有寫獲取方法代碼所以是值拿到變量沒有拿到方法)
aBoll 這個變量前面是添加了dynamic關鍵字的,我們獲取到了。在aInt這個變量前面我們是沒有添加的,所以可以看到我們是沒有獲取到這個變量的,那關鍵的就是我們要理解:dynamic 關鍵字的含義:
首先有 @objc 這個關鍵字,它是用來將Swift的API導出來給 Object-C 和 Runtime 使用的,如果你類繼承自OC的類,這個標識符就會被自動加進去,加了這標識符的屬性、方法無法保證都會被運行時調用,因為Swift會做靜態優化,想要完全被聲明成動態調用,必須使用 dynamic 標識符修飾,當然添加了 dynamic 的時候,它會自己在加上@objc這個標識符。
這樣我們就理解了dynamic這個關鍵字,知道了它的作用,那我們接下來就是嘗試着多使用一下 Swift Runtime。
Swift Runtime
上面解釋了這個關鍵字之后關於Swift的Runtime方面的只是就有了一個基本的了解了,下面的這些代碼就像我們整理OC Runtime 那樣也整理出來:
1、獲取方法:
let mthList = class_copyMethodList(object_getClass(SwiftClass),&count)
for index in 0..<numericCast(count) {
let method = method_getName(mthList?[index])
print("屬性成員方法:%@",String.init(NSStringFromSelector(method!)) ?? "沒有找到你要的方法")
}
2、屬性成員變量
let IvarList = class_copyIvarList(object_getClass(SwiftClass),&count)
for index in 0..<numericCast(count) {
let Ivar = ivar_getName(IvarList?[index])
print("屬性成員變量:%@",String.init(utf8String: Ivar!) ?? "沒有找到你想要的成員變量")
}
3、協議列表
let protocalList = class_copyProtocolList(object_getClass(self),&count)
for index in 0..<numericCast(count) {
let protocal = protocol_getName(protocalList?[index])
print("協議:%@",String.init(utf8String: protocal!) ?? "沒有找到你想要的協議")
}
4、方法交換
這個就是Runtime的一個重點了,仔細說一說。
OC的動態性最常用的其實就是方法的替換,將某個類的方法替換成自己定義的類,從而達到Hook的作用。(以前面試有人問過OC怎樣Hook一個消息,那時候太懵懂,不知道怎么說!不知道大家有沒有遇到過?)
對於純粹的Swift類,由於前面的測試你知道無法拿到類的屬性飯方法等,也就沒辦法進行方法的替換,但是對於繼承自NSObject的類,由於集成了OC的所有特性,所以是可以利用Runtime的屬性來進行方法替換,記得我們前面說的dynamic關鍵字。
func ChangeMethod() -> Void {
// 獲取交換之前的方法
let originaMethodC = class_getInstanceMethod(object_getClass(self), #selector(self.originaMethod))
// 獲取交換之后的方法
let swizzeMethodC = class_getInstanceMethod(object_getClass(self), #selector(self.swizzeMethod))
//替換類中已有方法的實現,如果該方法不存在添加該方法
//獲取方法的Type字符串(包含參數類型和返回值類型)
//class_replaceMethod(object_getClass(self), #selector(self.swizzeMethod), method_getImplementation(originaMethodC), method_getTypeEncoding(originaMethodC))
print("你交換兩個方法的實現")
method_exchangeImplementations(originaMethodC, swizzeMethodC)
}
dynamic func originaMethod() -> Void {
print("我是交換之前的方法")
}
dynamic func swizzeMethod() -> Void {
print("我是交換之后的方法")
}
5、關聯屬性
說上面的方法Hook比較重要的話,這個關聯屬性也是比較重要的,在前面我總結OC的Runtime的時候在方法的添加這里專門有提過一個Demo,我們把這個Demo重新整理一下,導航的漸變就是利用Runtime給導航添加屬性來實現的。
extension UINavigationBar {
var navigationGradualChangeBackgroundView:UIView?{
get{
return objc_getAssociatedObject(self, &self.navigationGradualChangeBackgroundView) as? UIView;
}
set{
objc_setAssociatedObject(self, &self.navigationGradualChangeBackgroundView, navigationGradualChangeBackgroundView, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
func setNavigationBackgroundColor (backgroundColor: UIColor) -> Void {
if (self.navigationGradualChangeBackgroundView == nil) {
self.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
self.navigationGradualChangeBackgroundView = UIView.init(frame: CGRect.init(x: 0, y: -20, width: SCREENWIDTH, height: self.bounds.size.height + 20))
self.navigationGradualChangeBackgroundView!.isUserInteractionEnabled = false
self.insertSubview(self.navigationGradualChangeBackgroundView!, at: 0)
}
self.navigationGradualChangeBackgroundView!.backgroundColor = backgroundColor
}
func removeNavigationBackgroundColor() -> Void {
self.setBackgroundImage(nil, for: UIBarMetrics.default)
self.navigationGradualChangeBackgroundView!.removeFromSuperview()
self.navigationGradualChangeBackgroundView = nil
}
}
1、上面是給UINavigationBar添加擴展來寫的,注意Swift的寫法和OC的區別。
2、在應用這點知識的時候,可以直接在ScrollView滾動的代理方法里面通過滾動距離的改變透明度生成你需要的Color,然后直接就在它的代理方法中調用setNavigationBackgroundColor方法即可。
看個其他的例子
在整理資料的時候,發現了一篇文章: iOS---防止UIButton重復點擊的三種實現方式
在最后面說道的利用Runtime的方法解決的時候,最后是這樣一段代碼:

說明:
可以看到最后是直接把自己定義的方法和系統的方法交換了,重點就是自己方法里面的實現!
可以看到在自己定義的方法前面加了時間判斷,最后還是調用了方法本身!這樣就有了一個問題。你用自己的方法代替了系統的方法,加入了自己的一些東西,最有沒有再去調用系統的方法?你不知道系統方法實現的具體內容卻直接用自己的方法規代替了,那系統按鈕的功能肯定是受到影響的!大家應該能理解我說的意思。那我們就得記得一點:
切記: 我們使用 Method Swizzling(方法交換) 的目的通常都是為了給程序增加功能,而不是完全地替換某個功能,所以我們一般都需要在自定義的實現中調用原始的實現。
針對這一點特別說明一下,怎么修改的其實原文下面的同學也有給出了答案的,具體的內容建議大家看看這篇文章,應該會有收獲!
Objective-C Method Swizzling 的最佳實踐
