標籤:成員 img bool http text extension 存在 iat list
你肯定也想過
在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擷取到它的屬性和方法
針對上面給出的結論,我們在看看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這個標識符。
Runtime
上面解釋了這個關鍵字之後關於Swift的Runtime方面的只是就有了一個基本的瞭解了,下面的這些代碼就像OC的一樣我們也整理出來:
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的作用。
對於純粹的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、在應用的時候直接在滾動的時候通過滾動距離的改變產生Color,在滾動的方法中調用setNavigationBackgroundColor方法即可。
看個其他的例子
在整理資料的時候,發現了一篇文章: iOS---防止UIButton重複點擊的三種實現方式
在最後面說道的利用Runtime的方法解決的時候,最後是這樣一段代碼:
說明:
可以看到最後是直接把自己定義的方法和系統的方法交換了,重點就是自己方法裡面的實現!可以看到在方法的前面加了時間的判斷,最後還是調用了方法本身!這樣就有了一個問題。你用自己的方法代替了系統的方法,加入了自己的一些東西,最有沒有再去調用系統的方法?你不知道系統方法實現的具體內容卻直接用自己的方法規代替了,那系統按鈕的功能肯定是受到影響的!應該能理解我說的意思吧。
切記: 我們使用 Method Swizzling(方法交換) 的目的通常都是為了給程式增加功能,而不是完全地替換某個功能,所以我們一般都需要在自訂的實現中調用原始的實現。
針對這一點特別說明一下,怎麼修改的其實文章下面的同學也給出了答案,具體的內容建議大家看看這篇文章,應該會有收穫!
Objective-C Method Swizzling 的最佳實務
Swift Runtime ?