美文网首页
Swift中Runtime简单了解以及项目中的运用

Swift中Runtime简单了解以及项目中的运用

作者: 哈南 | 来源:发表于2017-03-28 13:37 被阅读571次

最近没什么任务,想了解一下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)

控制台输出:

变量名.png
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)

控制台输出:


属性名.png

可以发现属性列表名的输出,就少了一个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))" )
        }

控制台输出:


方法名.png
(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!)

运行后控制台的输出:


添加属性.png
(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")))

控制台输出:

添加方法.png
也许你会有一些疑问,你添加的方法明明没有参数,为什么输出是两个参数,这两个参数的type我看了一下一个是“@”,一个是“:”,至于为什么默认带这两个参数,我也不知道。。
关于performSelector详解看这里:performSelector的详解
测试代码:添加属性已经添加方法代码在这里
(3)替换一个类的方法的实现

交换两个方法的IMP,这种方式也叫作Method Swizzling,Method Swizzling是iOS中AOP(面相切面编程)的一种实现方式。
方法Method的定义是:


Method的定义.png

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

Method Swizzling 1.png Method Swizzling 2.png

其实就是把方法内存放的实现地址给交换存储了一下。
进行交换的代码是:

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方法。
控制台输出:


jyViewDidAppear.png

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。
这个思路的代码,接下来我会重新写一篇文章仔细介绍一下,就不在这里展开了。
关于跳转的改进版文章:改进版
前后一共写了两天半,算是给自己前段时间学习的一个交代了。

相关文章

网友评论

      本文标题:Swift中Runtime简单了解以及项目中的运用

      本文链接:https://www.haomeiwen.com/subject/glrugttx.html