Swift-Runtime总结
前言
相信每一个iOS开发者都知道Runtime, 现在Swift也更新到5.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 ,代码如下:
1 2 3 4 5 6 7 8 9 | class TestASwiftClass { var aBoll : Bool = true var aInt : Int = 0 func testReturnVoidWithaId ( aId : UIView ) { print ( "TestASwiftClass.testReturnVoidWithaId" ) } } |
代码也是很简单,我们定义了两个变量一个方法,下面我们再写一个继承自 UIViewController的ViewController ,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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获取到它的属性和方法等等其他我们在OC中获得的东西。
针对上面给出的结论,我们看看Swift对于继承自OC的类是不是保留了OC所有的特性呢?再看下面代码,只是做一个简单的修改,把通过object_getClass方法获取的对象写成self:
1 2 3 4 5 6 | 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中定义的变量。这样也就证明了的确是上面答案说的那样。
那这样就又衍生出一个问题
那Swift就没办法利用Runtime了吗?
想一想,要是真的Swift没办法利用Runtime,那是一件得多让人失望的事!答案也肯定是否定的,我们还是能让Swift用Runtime的。看下面的代码:
1 2 3 4 5 6 7 8 9 | 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这个标识符。
这样我们就理解了dynamic这个关键字,知道了它的作用,那我们接下来就是尝试着多使用一下 Swift Runtime。
Swift Runtime
上面解释了这个关键字之后关于Swift的Runtime方面的只是就有了一个基本的了解了,下面的这些代码就像我们整理OC Runtime 那样也整理出来:
1、获取方法:
1 2 3 4 5 6 | 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、属性成员变量
1 2 3 4 5 6 | 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、协议列表
1 2 3 4 5 6 | 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的作用。(以前面试有人问过OC怎样Hook一个消息,那时候太懵懂,不知道怎么说!不知道大家有没有遇到过?)
对于纯粹的Swift类,由于前面的测试你知道无法拿到类的属性饭方法等,也就没办法进行方法的替换,但是对于继承自NSObject的类,由于集成了OC的所有特性,所以是可以利用Runtime的属性来进行方法替换,记得我们前面说的dynamic关键字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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给导航添加属性来实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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、在应用这点知识的时候,可以直接在ScrollView滚动的代理方法里面通过滚动距离的改变透明度生成你需要的Color,然后直接就在它的代理方法中调用setNavigationBackgroundColor方法即可。
看个其他的例子
在整理资料的时候,发现了一篇文章: iOS---防止UIButton重复点击的三种实现方式
在最后面说道的利用Runtime的方法解决的时候,最后是这样一段代码:
说明:
可以看到最后是直接把自己定义的方法和系统的方法交换了,重点就是自己方法里面的实现!
可以看到在自己定义的方法前面加了时间判断,最后还是调用了方法本身!这样就有了一个问题。你用自己的方法代替了系统的方法,加入了自己的一些东西,最有没有再去调用系统的方法?你不知道系统方法实现的具体内容却直接用自己的方法规代替了,那系统按钮的功能肯定是受到影响的!大家应该能理解我说的意思。那我们就得记得一点:
切记: 我们使用 Method Swizzling(方法交换) 的目的通常都是为了给程序增加功能,而不是完全地替换某个功能,所以我们一般都需要在自定义的实现中调用原始的实现。
针对这一点特别说明一下,怎么修改的其实原文下面的同学也有给出了答案的,具体的内容建议大家看看这篇文章,应该会有收获!
Objective-C Method Swizzling 的最佳实践
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!