最近没什么任务,想了解一下Swift中Runtime的一些知识,所以网上找了不少相关文章看了一下,算是了解了一些Swift中Runtime的知识,但是底层其实还是不太懂,只是了解一些Swift中Runtime的使用,所以想写一点东西,算是自己学习的总结。
Runtime能做什么呢?我自己总结了一下,主要功能有:
(一):网络请求解析数据转Model;自定义类型的归档,解档;某些情况KVC之前的check保护。这块主要用到的是获取目标类的属性列表,然后通过属性名进行判断,KVC等操作。
核心代码是:
//获取目标类所有属性
let propertys = class_copyPropertyList(object, &count)
(二):可以在运行时,给目标类添加一个方法。
核心代码:
let _ = class_addMethod(object_getClass(p), #selector(ViewController.readBook), class_getMethodImplementation(object_getClass(self), #selector(ViewController.readBook)), method_getTypeEncoding(method))
(三):在一个类的extension里面,给其添加属性。
核心代码是:
var bookName: String? {
set {
objc_setAssociatedObject(self, ViewController.bookName, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
get {
return objc_getAssociatedObject(self, ViewController.bookName) as? String
}
}
(四):在一个类的extension里面,对方法实现进行替换。
核心代码是:
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)
}
第一部分:获取目标类的属性,变量,方法,遵循代理列表
首先我创建了一个带有一些属性的People类,写了一些属性,方法,定义了一个代理。
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)获取目标类的变量列表:
苹果提供的方法是:
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)
控制台输出:

Ivar是objc_ivar的指针,包含变量名,变量类型等成员。
关于Ivar详解看这里:Ivar详解
(2)获取目标类的属性列表:
苹果提供的方法是:
public func class_copyPropertyList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> UnsafeMutablePointer<objc_property_t?>!
在OC中一个类有变量和属性的区别,但是在swift中并不会有这种区分,所以其实class_copyIvarList和class_copyPropertyList我实验了一下,获取到的东西基本一致,只是在class_copyIvarList里面会额外拿到自定义代理的Deleagte对象,其他还没发现额外的区别。
获取目标类属性名代码是:
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)
控制台输出:

可以发现属性列表名的输出,就少了一个delegate,这个对象是我在People里面写的自定义代理对象。
(3)获取目标类的方法列表
苹果提供的方法是:
public func class_copyMethodList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> UnsafeMutablePointer<Method?>!
所以获取目标类方法列表代码:
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!)"+"参数个数:\(Int(argument))" )
}
控制台输出:

(4)获取目标类遵循代理列表
苹果提供的方法是
public func class_copyProtocolList(_ cls: Swift.AnyClass!, _ outCount: UnsafeMutablePointer<UInt32>!) -> AutoreleasingUnsafeMutablePointer<Protocol?>!
获取目标类遵循代理主要代码是:
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("遵循的协议:\(name!)")
}
测试代码:上面代码在这里
第二部分:通过Runtime给目标类添加属性,添加方法,替换方法的实现
(1)给目标类添加属性
主要方法是:
public func objc_setAssociatedObject(_ object: Any!, _ key: UnsafeRawPointer!, _ value: Any!, _ policy: objc_AssociationPolicy)
第一个参数:目标类对象
第二个参数:所要添加的属性名
第三个参数:所要添加属性的值
第四个参数:采用的协议 objc_AssociationPolicy类型的值
首先我写了一个空的Person类,除了声明它是个类,就没进行其他操作了
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
}
}
}
这样我就添加了一个名为bookName的属性。我在其他地方调用的时候可以直接进行赋值和取值
let onePerson = Person()
onePerson.bookName = "邓小平传"
print(onePerson.bookName!)
运行后控制台的输出:

(2)给目标类添加方法
还是上面那个空Person类,给Person添加方法的主要代码是:
首先我在一个第三方类Viewcontroller里面写了一个readBook方法,里面进行一句输出
func readBook(){
print("I LOVE ReadBook")
}
然后把这个readBook的实现添加到Person类里面
let onePerson = Person()
//这句话是我写的一个工具类,简单的输出类存在的属性,方法等
Swift_RunTimeTool.getMethodList(object: Person.self)
//下面连个语句都是添加方法,第一个语句是添加能直接取到Sel的方法,后面的参数以及返回值是通过现有方法取出
let _ = class_addMethod(object_getClass(onePerson), #selector(ViewController.readBook), class_getMethodImplementation(object_getClass(self), #selector(ViewController.readBook)), method_getTypeEncoding(method))
//这个语句是添加取不到Sel的方法,添加一个目前不存在的方法名的方法,后面方法参数返回值是直接字符串给出
let _ = class_addMethod(object_getClass(onePerson), Selector(("findBook")), class_getMethodImplementation(object_getClass(self), #selector(ViewController.readBook)), "v@:")
Swift_RunTimeTool.getMethodList(object: Person.self)
//添加后的方法的调用只能是通过performSelector
onePerson.perform(Selector(("findBook")))
控制台输出:

也许你会有一些疑问,你添加的方法明明没有参数,为什么输出是两个参数,这两个参数的type我看了一下一个是“@”,一个是“:”,至于为什么默认带这两个参数,我也不知道。。
关于performSelector详解看这里:performSelector的详解
测试代码:添加属性已经添加方法代码在这里
(3)替换一个类的方法的实现
交换两个方法的IMP,这种方式也叫作Method Swizzling,Method Swizzling是iOS中AOP(面相切面编程)的一种实现方式。
方法Method的定义是:

Method Swizzling简略的过程就是如下面两张图:


其实就是把方法内存放的实现地址给交换存储了一下。
进行交换的代码是:
public func method_exchangeImplementations(_ m1: Method!, _ m2: Method!)
所以对于两个方法具体的交换代码是:
/// 交换一个类的两个方法实现
///
/// - Parameters:
/// - cls: 目标类
/// - 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)
}
}
这个方法被我写在了一个Swift_RunTimeTool.swift文件里面,自后会通过这个类直接去调用交换方法。
接下来我把UIViewController的ViewDidAppear方法和我自己写的一个jyViewDidAppear的方法交换一下,等于在ViewDidAppear加入一些自己的东西。
import Foundation
import UIKit
extension UIViewController{
override open class func initialize(){
super.initialize()
//在swift3.0中GCD的once方法被苹果删除了,所以我对DispatchQueue添加了一个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){
//这样调用不会导致递归,运行后self.jyViewDidAppear(animation)真正调用的是ViewDidAppear方法,因为系统的方法里面会进行一些必要的操作,不能忽略不调用。
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()
}
}
这样代码写过之后,当VC里面的View出现之后,会调用jyViewDidAppear方法。
控制台输出:

Method Swizzling详解看这里:iOS黑魔法-Method Swizzling
这篇文章介绍了一个挺有用的通过Runtime交换方法防止NSArray取值越界,但是在swift中Array变成结构体了,不再是OC中的继承NSObject类,所以OC中通过替换NSArray的objectAtIndex方法的行为就不可行了。
第三部分:Runtime在项目中的运用
下面是我觉得Runtime在项目里面最有用的两种运用方式,可以更好更优雅的实现项目中某些功能
(1)通过Runtime解耦合统计埋点
这个功能的实现简单点讲就是通过替换UIViewController和UIControl中的系统方法,在系统调用这些方法时,我们加入自己的统计代码。
extension UIViewController{
override open class func initialize(){
super.initialize()
DispatchQueue.once(token: "com.besttone.jyBesttone") {
//交换viewDidAppear和viewWillDisappear方法,统计每个页面进入和离开,然后再通过plist文件直接取对应的统计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文件获取一个页面VC的统计ID,然后在这个方法里面进行统计请求的发起
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统计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()
}
}
这样在每个VC进入和离开会调用到,我自己写的jyViewDidAppear和jyViewWillDisAppear,就可以进行统计操作。其实这个方法是看别人的文章获得的,感觉这种写法真的挺不错,文章还写道了替换Control里面的事件方法,获取Control子类,不同UIbutton的点击事件,可以根据Button所在的文件和点击绑定的事件来进行区分,然后同样的也是在事先设置好的plist文件去获取到Control的统计ID。我是参考别人文章的思路,他的文章比我说的细。
我参考的文章在这里:可复用而且高度解耦的用户统计埋点实现
我最终写出来的代码:代码在这里
(2)push从任何一个界面跳转任何一个界面
这个东西个人感觉还是很有用的,在和后台对接好后,对于推送跳转的写法就会方便很多。这块最重要的东西其实不是Runtime的东西,Runtime更多的是做一个KVC之前的保护,防止设置空key导致崩溃。
首先在AppDelegate里面写一个处理push信息的方法
func conductPushParams(params : [String : AnyObject]){
guard let className = params["class"] as? String else{
print("参数类名不存在")
return
}
//读取本地Storyboard绑定VC的ID的plist文件
let filePath = Bundle.main.path(forResource: "VCStoryboardID", ofType: "plist") ?? ""
let vcDic : NSDictionary = NSDictionary(contentsOfFile: filePath) ?? NSDictionary()
//判断plist文件里面是否含有目标VC的Storyboard ID
if let vcID = vcDic[className] {
//如果VC是与storyboard关联的通过下面的语句创建VC对象,通过Runtime检查VC对象是否含有对应属性,如果包含通过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结构的 如果不是这个结构下面这句as! UITabBarController强转你就会崩溃
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关联,则通过Push传过来的目标VC名字创建VC对象,通过Runtime检查VC对象是否含有对应属性,如果包含通过KVC给属性赋值
guard let clsName = Bundle.main.infoDictionary!["CFBundleExecutable"] else {
print("命名空间不存在")
return
}
//swift根据VC名字字符串创建相应VC的对象,首先获取到命名空间,然后NSClassFromString(命名空间.VC名字字符串),就能获取到相应的VC,class
let classTemp : AnyClass? = NSClassFromString((clsName as! String) + "." + className)
guard let clsType = classTemp as? UIViewController.Type else{
print("无法转换为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
}
}
然后继续在AppDelete写一个模拟推送信息的方法
func setPushParams() ->[String : AnyObject]{
let params = ["class" : "TestViewController" , "property" : ["sourceText" : "推送跳转过来"] ] as [String : Any]
return params as [String : AnyObject]
}
class:是你要跳转的目的VC的名字,需要我们和后台进行对接好,不能随便推,如果觉得不好维护,也可以规定好ID,然后项目里面设置plist文件,从plist文件里面获取到VC的名字字符串。
property:推送带过来的参数信息
然后我们在didFinishLaunchingWithOptions方法里面延时调用一下,模拟推送。
func application(_ application: UIApplication, didFinishLaunchingWithOptions
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
self.conductPushParams(params: self.setPushParams())
}
}
这些代码都是一个实验的东西,在实际项目里面不可能只传一个字符串,肯定会带有不少信息,这样就需要传一个model了,我们可以设置一个pushModel的东西,里面写上需要推送跳转过去的VC的dataModel作为属性,然后根据dataModel的具体名字创建对象,根据推送过来的key和值,kvc设置dataModel值,然后将dataModel通过kvc赋值给pushModel的相应属性,最后将pushModel的通过kvc赋值给需要跳转的VC。
这个思路的代码,接下来我会重新写一篇文章仔细介绍一下,就不在这里展开了。
关于跳转的改进版文章:改进版
前后一共写了两天半,算是给自己前段时间学习的一个交代了。
网友评论