当前位置:首页 » PHP技术

Swift Runtime ?

2017-12-07 16:20 本站整理 浏览(19)

你肯定也想过


  在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 的最佳实践