最近沒(méi)什么任務(wù),想了解一下Swift中Runtime的一些知識(shí),所以網(wǎng)上找了不少相關(guān)文章看了一下,算是了解了一些Swift中Runtime的知識(shí),但是底層其實(shí)還是不太懂,只是了解一些Swift中Runtime的使用,所以想寫(xiě)一點(diǎn)東西,算是自己學(xué)習(xí)的總結(jié)。
Runtime能做什么呢?我自己總結(jié)了一下,主要功能有:
(一):網(wǎng)絡(luò)請(qǐng)求解析數(shù)據(jù)轉(zhuǎn)Model;自定義類型的歸檔,解檔;某些情況KVC之前的check保護(hù)。這塊主要用到的是獲取目標(biāo)類的屬性列表,然后通過(guò)屬性名進(jìn)行判斷,KVC等操作。
核心代碼是:
//獲取目標(biāo)類所有屬性
let propertys = class_copyPropertyList(object, &count)
(二):可以在運(yùn)行時(shí),給目標(biāo)類添加一個(gè)方法。
核心代碼:
let _ = class_addMethod(object_getClass(p), #selector(ViewController.readBook), class_getMethodImplementation(object_getClass(self), #selector(ViewController.readBook)), method_getTypeEncoding(method))
(三):在一個(gè)類的extension里面,給其添加屬性。
核心代碼是:
var bookName: String? {
set {
objc_setAssociatedObject(self, ViewController.bookName, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
get {
return objc_getAssociatedObject(self, ViewController.bookName) as? String
}
}
(四):在一個(gè)類的extension里面,對(duì)方法實(shí)現(xiàn)進(jìn)行替換。
核心代碼是:
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)
}
第一部分:獲取目標(biāo)類的屬性,變量,方法,遵循代理列表
首先我創(chuàng)建了一個(gè)帶有一些屬性的People類,寫(xiě)了一些屬性,方法,定義了一個(gè)代理。
import UIKit
class People:NSObject,UITableViewDelegate {
var name:String = ""
var sex:String = ""
var age:Int = 0
var height:Float = 0.0
var job:String = ""
var native:String = ""
var education:String = ""
override init() {
super.init()
}
var delegate:PeopleDelegate?
func eat(){
delegate?.peopleDelegateToWork()
}
func sleep(){
}
func work(){
}
}
protocol PeopleDelegate {
func peopleDelegateToWork()
}
(1)獲取目標(biāo)類的變量列表:
蘋(píng)果提供的方法是:
public func class_copyIvarList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> UnsafeMutablePointer<Ivar?>!
所以獲取變量名的代碼是:
var count:UInt32 = 0
let ivars = class_copyIvarList(People.self, &count)
for i in 0 ..< Int(count) {
let ivar = ivars?[i]
if ivar != nil{
let tempName = String(cString: ivar_getName(ivar!))
print("變量名:\(tempName)")
}
}
free(ivars)
控制臺(tái)輸出:
Ivar是objc_ivar的指針,包含變量名,變量類型等成員。
關(guān)于Ivar詳解看這里:Ivar詳解
(2)獲取目標(biāo)類的屬性列表:
蘋(píng)果提供的方法是:
public func class_copyPropertyList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> UnsafeMutablePointer<objc_property_t?>!
在OC中一個(gè)類有變量和屬性的區(qū)別,但是在swift中并不會(huì)有這種區(qū)分,所以其實(shí)class_copyIvarList和class_copyPropertyList我實(shí)驗(yàn)了一下,獲取到的東西基本一致,只是在class_copyIvarList里面會(huì)額外拿到自定義代理的Deleagte對(duì)象,其他還沒(méi)發(fā)現(xiàn)額外的區(qū)別。
獲取目標(biāo)類屬性名代碼是:
var count:UInt32 = 0
let properties = class_copyPropertyList(People.self, &count)
for i in 0 ..< Int(count){
let property = properties?[i];
if property != nil{
let propertyName = String(cString: property_getName(property!))
print("屬性名:\(propertyName)")
}
}
free(properties)
控制臺(tái)輸出:
可以發(fā)現(xiàn)屬性列表名的輸出,就少了一個(gè)delegate,這個(gè)對(duì)象是我在People里面寫(xiě)的自定義代理對(duì)象。
(3)獲取目標(biāo)類的方法列表
蘋(píng)果提供的方法是:
public func class_copyMethodList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> UnsafeMutablePointer<Method?>!
所以獲取目標(biāo)類方法列表代碼:
var count:UInt32 = 0
let funcs = class_copyMethodList(People.self, &count)
for i in 0 ..< Int(count){
let sel = sel_getName(method_getName(funcs?[i]))
let name = String.init(validatingUTF8: sel!)
let argument = method_getNumberOfArguments(funcs?[i])
print("方法名:\(name!)"+"參數(shù)個(gè)數(shù):\(Int(argument))" )
}
控制臺(tái)輸出:
(4)獲取目標(biāo)類遵循代理列表
蘋(píng)果提供的方法是
public func class_copyProtocolList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> AutoreleasingUnsafeMutablePointer<Protocol?>!
獲取目標(biāo)類遵循代理主要代碼是:
var count:UInt32 = 0
let protocolArray = class_copyProtocolList(People.self, &count)
for index in 0 ..< Int(count){
let protocolTemp = protocol_getName(protocolArray?[index])
let name = String.init(validatingUTF8: protocolTemp!)
print("遵循的協(xié)議:\(name!)")
}
測(cè)試代碼:上面代碼在這里
第二部分:通過(guò)Runtime給目標(biāo)類添加屬性,添加方法,替換方法的實(shí)現(xiàn)
(1)給目標(biāo)類添加屬性
主要方法是:
public func objc_setAssociatedObject(_ object: Any!, _ key: UnsafeRawPointer!, _ value: Any!, _ policy: objc_AssociationPolicy)
第一個(gè)參數(shù):目標(biāo)類對(duì)象
第二個(gè)參數(shù):所要添加的屬性名
第三個(gè)參數(shù):所要添加屬性的值
第四個(gè)參數(shù):采用的協(xié)議 objc_AssociationPolicy類型的值
首先我寫(xiě)了一個(gè)空的Person類,除了聲明它是個(gè)類,就沒(méi)進(jìn)行其他操作了
class Person : NSObject{
}
在extension里面添加的代碼是:
extension Person{
var bookName: String? {
set {
objc_setAssociatedObject(self, ViewController.bookName, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
}
get {
return objc_getAssociatedObject(self, ViewController.bookName) as? String
}
}
}
這樣我就添加了一個(gè)名為bookName的屬性。我在其他地方調(diào)用的時(shí)候可以直接進(jìn)行賦值和取值
let onePerson = Person()
onePerson.bookName = "鄧小平傳"
print(onePerson.bookName!)
運(yùn)行后控制臺(tái)的輸出:
(2)給目標(biāo)類添加方法
還是上面那個(gè)空Person類,給Person添加方法的主要代碼是:
首先我在一個(gè)第三方類Viewcontroller里面寫(xiě)了一個(gè)readBook方法,里面進(jìn)行一句輸出
func readBook(){
print("I LOVE ReadBook")
}
然后把這個(gè)readBook的實(shí)現(xiàn)添加到Person類里面
let onePerson = Person()
//這句話是我寫(xiě)的一個(gè)工具類,簡(jiǎn)單的輸出類存在的屬性,方法等
Swift_RunTimeTool.getMethodList(object: Person.self)
//下面連個(gè)語(yǔ)句都是添加方法,第一個(gè)語(yǔ)句是添加能直接取到Sel的方法,后面的參數(shù)以及返回值是通過(guò)現(xiàn)有方法取出
let _ = class_addMethod(object_getClass(onePerson), #selector(ViewController.readBook), class_getMethodImplementation(object_getClass(self), #selector(ViewController.readBook)), method_getTypeEncoding(method))
//這個(gè)語(yǔ)句是添加取不到Sel的方法,添加一個(gè)目前不存在的方法名的方法,后面方法參數(shù)返回值是直接字符串給出
let _ = class_addMethod(object_getClass(onePerson), Selector(("findBook")), class_getMethodImplementation(object_getClass(self), #selector(ViewController.readBook)), "v@:")
Swift_RunTimeTool.getMethodList(object: Person.self)
//添加后的方法的調(diào)用只能是通過(guò)performSelector
onePerson.perform(Selector(("findBook")))
控制臺(tái)輸出:
也許你會(huì)有一些疑問(wèn),你添加的方法明明沒(méi)有參數(shù),為什么輸出是兩個(gè)參數(shù),這兩個(gè)參數(shù)的type我看了一下一個(gè)是“@”,一個(gè)是“:”,至于為什么默認(rèn)帶這兩個(gè)參數(shù),我也不知道。。
關(guān)于performSelector詳解看這里:performSelector的詳解
測(cè)試代碼:添加屬性已經(jīng)添加方法代碼在這里
(3)替換一個(gè)類的方法的實(shí)現(xiàn)
交換兩個(gè)方法的IMP,這種方式也叫作Method Swizzling,Method Swizzling是iOS中AOP(面相切面編程)的一種實(shí)現(xiàn)方式。
方法Method的定義是:
Method Swizzling簡(jiǎn)略的過(guò)程就是如下面兩張圖:
其實(shí)就是把方法內(nèi)存放的實(shí)現(xiàn)地址給交換存儲(chǔ)了一下。
進(jìn)行交換的代碼是:
public func method_exchangeImplementations(_ m1: Method!, _ m2: Method!)
所以對(duì)于兩個(gè)方法具體的交換代碼是:
/// 交換一個(gè)類的兩個(gè)方法實(shí)現(xiàn)
///
/// - Parameters:
/// - cls: 目標(biāo)類
/// - originalSelector:被交換的方法
/// - swizzeSelector: 交換的方法
class func methodSwizze(cls : AnyClass,originalSelector : Selector , swizzeSelector : Selector) {
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzeMethod = class_getInstanceMethod(cls, swizzeSelector)
let didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzeMethod), method_getTypeEncoding(swizzeMethod))
if didAddMethod {
class_replaceMethod(cls, swizzeSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
}else {
method_exchangeImplementations(originalMethod, swizzeMethod)
}
}
這個(gè)方法被我寫(xiě)在了一個(gè)Swift_RunTimeTool.swift文件里面,自后會(huì)通過(guò)這個(gè)類直接去調(diào)用交換方法。
接下來(lái)我把UIViewController的ViewDidAppear方法和我自己寫(xiě)的一個(gè)jyViewDidAppear的方法交換一下,等于在ViewDidAppear加入一些自己的東西。
import Foundation
import UIKit
extension UIViewController{
override open class func initialize(){
super.initialize()
//在swift3.0中GCD的once方法被蘋(píng)果刪除了,所以我對(duì)DispatchQueue添加了一個(gè)once方法
DispatchQueue.once(token: "com.besttone.jySwizzeMethod") {
let primaryAppearSel = #selector(UIViewController.viewDidAppear(_:))
let replaceAppearSel = #selector(UIViewController.jyViewDidAppear(_:))
SwiftRunTimeTool.methodSwizze(cls: UIViewController.self, originalSelector: primaryAppearSel, swizzeSelector: replaceAppearSel)
}
func jyViewDidAppear(_ animation : Bool){
//這樣調(diào)用不會(huì)導(dǎo)致遞歸,運(yùn)行后self.jyViewDidAppear(animation)真正調(diào)用的是ViewDidAppear方法,因?yàn)橄到y(tǒng)的方法里面會(huì)進(jìn)行一些必要的操作,不能忽略不調(diào)用。
self.jyViewDidAppear(animation)
print("jyViewDidAppear")
}
public extension DispatchQueue {
private static var _onceTracker = [String]()
public class func once(token: String, block:()->Void) {
objc_sync_enter(self)
defer {
objc_sync_exit(self) }
if _onceTracker.contains(token) {
return
}
_onceTracker.append(token)
block()
}
}
這樣代碼寫(xiě)過(guò)之后,當(dāng)VC里面的View出現(xiàn)之后,會(huì)調(diào)用jyViewDidAppear方法。
控制臺(tái)輸出:
Method Swizzling詳解看這里:iOS黑魔法-Method Swizzling
這篇文章介紹了一個(gè)挺有用的通過(guò)Runtime交換方法防止NSArray取值越界,但是在swift中Array變成結(jié)構(gòu)體了,不再是OC中的繼承NSObject類,所以O(shè)C中通過(guò)替換NSArray的objectAtIndex方法的行為就不可行了。
第三部分:Runtime在項(xiàng)目中的運(yùn)用
下面是我覺(jué)得Runtime在項(xiàng)目里面最有用的兩種運(yùn)用方式,可以更好更優(yōu)雅的實(shí)現(xiàn)項(xiàng)目中某些功能
(1)通過(guò)Runtime解耦合統(tǒng)計(jì)埋點(diǎn)
這個(gè)功能的實(shí)現(xiàn)簡(jiǎn)單點(diǎn)講就是通過(guò)替換UIViewController和UIControl中的系統(tǒng)方法,在系統(tǒng)調(diào)用這些方法時(shí),我們加入自己的統(tǒng)計(jì)代碼。
extension UIViewController{
override open class func initialize(){
super.initialize()
DispatchQueue.once(token: "com.besttone.jyBesttone") {
//交換viewDidAppear和viewWillDisappear方法,統(tǒng)計(jì)每個(gè)頁(yè)面進(jìn)入和離開(kāi),然后再通過(guò)plist文件直接取對(duì)應(yīng)的統(tǒng)計(jì)ID
let primaryAppearSel = #selector(UIViewController.viewDidAppear(_:))
let replaceAppearSel = #selector(UIViewController.jyViewDidAppear(_:))
SwiftRunTimeTool.methodSwizze(cls: UIViewController.self, originalSelector: primaryAppearSel, swizzeSelector: replaceAppearSel)
let primaryDisapperaSel = #selector(UIViewController.viewWillDisappear(_:))
let replaceDisapperaSel = #selector(UIViewController.jyViewWillDisAppear(_:))
SwiftRunTimeTool.methodSwizze(cls: UIViewController.self, originalSelector: primaryDisapperaSel, swizzeSelector: replaceDisapperaSel)
}
}
func jyViewDidAppear(_ animation : Bool){
self.jyViewDidAppear(animation)
//從本地plist文件獲取一個(gè)頁(yè)面VC的統(tǒng)計(jì)ID,然后在這個(gè)方法里面進(jìn)行統(tǒng)計(jì)請(qǐng)求的發(fā)起
if VCIDTool.getVCID(className: object_getClass(self), isEnter: true) != ""{
print(VCIDTool.getVCID(className: object_getClass(self), isEnter: true))
}
}
func jyViewWillDisAppear(_ animation : Bool){
self.jyViewWillDisAppear(animation)
if VCIDTool.getVCID(className: object_getClass(self), isEnter: false) != ""{
print(VCIDTool.getVCID(className: object_getClass(self), isEnter: false))
}
}
}
class VCIDTool : NSObject{
//從Plist文件取VC統(tǒng)計(jì)ID
class func getVCID(className : AnyClass , isEnter : Bool) -> String{
let filePath = Bundle.main.path(forResource: "ViewControllerIDList", ofType: "plist") ?? ""
let vcDic : NSDictionary = NSDictionary(contentsOfFile: filePath) ?? NSDictionary()
let vcName = "\(className)"
if let vcSomeDic = vcDic[vcName] {
return ((vcSomeDic as! NSDictionary)["PageEnentIDs"] as! NSDictionary)[isEnter ? "Enter" : "Leave"] as! String
}else{
return ""
}
}
}
public extension DispatchQueue {
private static var _onceTracker = [String]()
public class func once(token: String, block:()->Void) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
if _onceTracker.contains(token) {
return
}
_onceTracker.append(token)
block()
}
}
這樣在每個(gè)VC進(jìn)入和離開(kāi)會(huì)調(diào)用到,我自己寫(xiě)的jyViewDidAppear和jyViewWillDisAppear,就可以進(jìn)行統(tǒng)計(jì)操作。其實(shí)這個(gè)方法是看別人的文章獲得的,感覺(jué)這種寫(xiě)法真的挺不錯(cuò),文章還寫(xiě)道了替換Control里面的事件方法,獲取Control子類,不同UIbutton的點(diǎn)擊事件,可以根據(jù)Button所在的文件和點(diǎn)擊綁定的事件來(lái)進(jìn)行區(qū)分,然后同樣的也是在事先設(shè)置好的plist文件去獲取到Control的統(tǒng)計(jì)ID。我是參考別人文章的思路,他的文章比我說(shuō)的細(xì)。
我參考的文章在這里:可復(fù)用而且高度解耦的用戶統(tǒng)計(jì)埋點(diǎn)實(shí)現(xiàn)
我最終寫(xiě)出來(lái)的代碼:代碼在這里
(2)push從任何一個(gè)界面跳轉(zhuǎn)任何一個(gè)界面
這個(gè)東西個(gè)人感覺(jué)還是很有用的,在和后臺(tái)對(duì)接好后,對(duì)于推送跳轉(zhuǎn)的寫(xiě)法就會(huì)方便很多。這塊最重要的東西其實(shí)不是Runtime的東西,Runtime更多的是做一個(gè)KVC之前的保護(hù),防止設(shè)置空key導(dǎo)致崩潰。
首先在AppDelegate里面寫(xiě)一個(gè)處理push信息的方法
func conductPushParams(params : [String : AnyObject]){
guard let className = params["class"] as? String else{
print("參數(shù)類名不存在")
return
}
//讀取本地Storyboard綁定VC的ID的plist文件
let filePath = Bundle.main.path(forResource: "VCStoryboardID", ofType: "plist") ?? ""
let vcDic : NSDictionary = NSDictionary(contentsOfFile: filePath) ?? NSDictionary()
//判斷plist文件里面是否含有目標(biāo)VC的Storyboard ID
if let vcID = vcDic[className] {
//如果VC是與storyboard關(guān)聯(lián)的通過(guò)下面的語(yǔ)句創(chuàng)建VC對(duì)象,通過(guò)Runtime檢查VC對(duì)象是否含有對(duì)應(yīng)屬性,如果包含通過(guò)KVC給屬性賦值
let clsVC = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: vcID as! String)
for key in (params["property"] as! [String : String]).keys{
if SwiftRunTimeTool.checkClassPerporty(object: object_getClass(clsVC), propertyStr: key){
clsVC.setValue((params["property"] as! [String : String])[key]! as String, forKey: key)
}
}
//如果App是Tabbar + Navigation + VC結(jié)構(gòu)的 如果不是這個(gè)結(jié)構(gòu)下面這句as! UITabBarController強(qiáng)轉(zhuǎn)你就會(huì)崩潰
let tabVC = self.window?.rootViewController as! UITabBarController
let navVC = tabVC.viewControllers?[tabVC.selectedIndex] as! UINavigationController
clsVC.hidesBottomBarWhenPushed = true
navVC.pushViewController(clsVC, animated: true)
}else{
//如果VC不與Storyboard關(guān)聯(lián),則通過(guò)Push傳過(guò)來(lái)的目標(biāo)VC名字創(chuàng)建VC對(duì)象,通過(guò)Runtime檢查VC對(duì)象是否含有對(duì)應(yīng)屬性,如果包含通過(guò)KVC給屬性賦值
guard let clsName = Bundle.main.infoDictionary!["CFBundleExecutable"] else {
print("命名空間不存在")
return
}
//swift根據(jù)VC名字字符串創(chuàng)建相應(yīng)VC的對(duì)象,首先獲取到命名空間,然后NSClassFromString(命名空間.VC名字字符串),就能獲取到相應(yīng)的VC,class
let classTemp : AnyClass? = NSClassFromString((clsName as! String) + "." + className)
guard let clsType = classTemp as? UIViewController.Type else{
print("無(wú)法轉(zhuǎn)換為UIViewController類型")
return
}
let clsVC = clsType.init()
let tabVC = self.window?.rootViewController as! UITabBarController
let navVC = tabVC.viewControllers?[tabVC.selectedIndex] as! UINavigationController
for key in (params["property"] as! [String : String]).keys{
if SwiftRunTimeTool.checkClassPerporty(object: object_getClass(clsVC), propertyStr: key){
clsVC.setValue((params["property"] as! [String : String])[key]! as String, forKey: key)
}
}
clsVC.hidesBottomBarWhenPushed = true
navVC.pushViewController(clsVC, animated: true)
}
}
class SwiftRunTimeTool: NSObject {
class func checkClassPerporty(object : AnyClass , propertyStr: String) -> Bool{
var count : UInt32 = 0
let propertys = class_copyPropertyList(object, &count)
for index in 0 ..< Int(count){
let name = property_getName(propertys?[index])
if let propertyName = String.init(validatingUTF8: name!){
if propertyName == propertyStr{
return true
}
}
}
return false
}
}
然后繼續(xù)在AppDelete寫(xiě)一個(gè)模擬推送信息的方法
func setPushParams() ->[String : AnyObject]{
let params = ["class" : "TestViewController" , "property" : ["sourceText" : "推送跳轉(zhuǎn)過(guò)來(lái)"] ] as [String : Any]
return params as [String : AnyObject]
}
class:是你要跳轉(zhuǎn)的目的VC的名字,需要我們和后臺(tái)進(jìn)行對(duì)接好,不能隨便推,如果覺(jué)得不好維護(hù),也可以規(guī)定好ID,然后項(xiàng)目里面設(shè)置plist文件,從plist文件里面獲取到VC的名字字符串。
property:推送帶過(guò)來(lái)的參數(shù)信息
然后我們?cè)赿idFinishLaunchingWithOptions方法里面延時(shí)調(diào)用一下,模擬推送。
func application(_ application: UIApplication, didFinishLaunchingWithOptions
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
self.conductPushParams(params: self.setPushParams())
}
}
這些代碼都是一個(gè)實(shí)驗(yàn)的東西,在實(shí)際項(xiàng)目里面不可能只傳一個(gè)字符串,肯定會(huì)帶有不少信息,這樣就需要傳一個(gè)model了,我們可以設(shè)置一個(gè)pushModel的東西,里面寫(xiě)上需要推送跳轉(zhuǎn)過(guò)去的VC的dataModel作為屬性,然后根據(jù)dataModel的具體名字創(chuàng)建對(duì)象,根據(jù)推送過(guò)來(lái)的key和值,kvc設(shè)置dataModel值,然后將dataModel通過(guò)kvc賦值給pushModel的相應(yīng)屬性,最后將pushModel的通過(guò)kvc賦值給需要跳轉(zhuǎn)的VC。
這個(gè)思路的代碼,接下來(lái)我會(huì)重新寫(xiě)一篇文章仔細(xì)介紹一下,就不在這里展開(kāi)了。
關(guān)于跳轉(zhuǎn)的改進(jìn)版文章:改進(jìn)版
前后一共寫(xiě)了兩天半,算是給自己前段時(shí)間學(xué)習(xí)的一個(gè)交代了。