你肯定也想过
在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 的最佳实践
请发表评论