【Swift】Swift黑魔法 - Runtime

【Swift】Swift黑魔法 - Runtime,第1张

概述一、什么是Runtime: Runtime是苹果开发中比extension更加强大的一项黑科技 extension允许用户在不修改原始代码的情况下,为类增加额外的方法 而Runtime则允许用户在不修改原始代码的情况下,为类增加额外的属性,甚至直接改变原有方法的实现 *Runtime在OC和Swift中都可以使用 二、方法交叉:Method Swizzling 1、为什么要用Method Swiz 一、什么是Runtime: Runtime是苹果开发中比extension更加强大的一项黑科技 extension允许用户在不修改原始代码的情况下,为类增加额外的方法 而Runtime则允许用户在不修改原始代码的情况下,为类增加额外的属性,甚至直接改变原有方法的实现 *Runtime在OC和Swift中都可以使用
二、方法交叉:Method Swizzling 1、为什么要用Method Swizzling 在实际应用中,我们可能会遇到这样的场景: 我们定义了很多Label控件,有些是在xib中定义的,有些是通过代码创建的 在这些Label中,有些使用的是系统默认字体,有些使用的则是自定义字体 然后有一天,设计师突然给了一个新字体,说所有的系统默认字体都应该替换成这个字体 虽然很不爽,但还是花了半天时间把系统中所有的Label字体都修改了一遍(可能有些还有遗漏) 然后又有一天,老板说这个字体不好看!给我换成另外一个字体!于是设计师又给了一个新字体 于是又花了半天时间把所有Label的字体再修改一遍,并且出现了更多的遗漏 就这样,我们浪费了很多时间在改字体上面,最后出来的效果还不尽人意
于是我们就心想,如果有一个方法,能够把所有Label的系统默认字体,都替换成我们想要的新字体该多好啊 这样,不管我们在xib中定义Label还是在代码中创建Label,都直接使用系统默认字体即可 即使要改字体的话,也直接修改这个方法中的字体就行,然后所有Label就都自动改过来了
当然,这个需求是可以实现的,但首先要有解决问题的思路: 思路一:每当程序需要获取系统默认字体时,直接返回我们想要的字体 思路二:每当Label被创建或加载时,判断当前Label的字体是否为系统字体,若是,则修改为我们想要的字体
那么问题来了,无论用哪种思路,都要求必须要修改系统原始控件中的方法,例如 思路一:需要修改UIFont的systemFontOfSize等方法,使其返回指定的字体 思路二:需要修改UILabel的awakeFromNib等方法,将其修改为指定的字体
但是,我们并不能直接修改UIFont和UILabel的代码,那么要如何实现我们想要的效果呢? 这就要用到Runtime中的Method Swizzling机制了
2、什么是Method Swizzling: Swizzling的中文翻译是“交叉混合”,要想理解Method Swizzling机制,首先需要了解iOS中方法调用的原理
在iOS中,每个方法(或称为函数,下同)都是由方法的声明和方法的实现两部分来构成的 其中,方法的声明就是方法的名称和参数,而方法的实现就是方法中定义的代码 *为了简单起见,在下面的介绍中,方法的声明将简称为方法名,方法的实现将简称为方法体
在正常调用方法的时候,我们通常是将两者作为一个整体调用的,即通过方法名直接调用方法体 但实际上,方法名和方法体是两个不同的对象,方法名的对象类型为Selector,而方法体的对象类型则为Method 当我们调用一个方法时,系统首先会找到该方法对应的Selector,然后再根据Selector找到对应的Method 当然,我们也可以直接提供给系统一个Selector,系统也可以通过该Selector找到对应的Method 但是,如果我们把一个方法的Selector,指向另一个方法的Method,那会怎样呢?
为了理解这个概念,我们首先看看下面这个类:
class SwizzlingDemo {    func printA() {        print("A")    }        func printB () {        print("B")    }}
很明显: 当我们调用printA()的时候,控制台就会打印A 当我们调用printB()的时候,控制台就会打印B
那么,我们能不能在 完全不修改这个类的任何代码的前提下 在调用printA()的时候,让控制台打印B 而调用printB()的时候,让控制台打印A呢? 这样就需要用到Method Swizzling了
Method Swizzling,其作用就是 在不修改任何代码的前提下,交换两个方法的声明与实现 为了实现上述需求,我们首先需要使用extension,对SwizzlingDemo进行扩展:
extension Swizzling Demo {       class func swizzlePrintMethod() {        //获取printA和printB的Selector        let printASelector = #selector(SwizzlingDemo.printA)        let printBSelector = #selector(SwizzlingDemo.printB)                //获取printA和printB的Method        let printAMethod = class_getInstanceMethod(self,printASelector)        let printBMethod = class_getInstanceMethod(self,printBSelector)                //交换两个方法的Method        method_exchangeImplementations(printAMethod,printBMethod)    }}
我们在SwizzlingDemo中扩展了一个方法:swizzlePrintMethod() 从代码中可以很清晰地看出: 我们首先获取了printA和printB两个方法的Selector 再根据两个Selector取出了两个方法的Method 最后交换了两个方法的Method,Swizzing完毕。
是的,就是这么简单。 执行了这段代码之后,当我们再调用printA的时候 系统实际上执行的就是printB的Method,也就是在控制台打印B了 所以,问题就变成了,我们应该在什么时候调用这个方法呢?
一个最推荐(也是最安全)的做法是: 重写这个类的initialize方法,并且通过dispatch_once仅调用一次,如下:
extension SwizzlingDemo {    //首先我们需要重写initialize方法,当系统在加载这个类的时候就会执行其中的代码    overrIDe class func initialize() {       //创建单次标识(固定写法,照抄即可)       struct Static {           static var token: dispatch_once_t =0       }               //使用单次标识代码块,确保其中的代码仅执行一次(固定写法,照抄即可)       //*重要!*此代码块不可省略!否则会产生不可预知的崩溃!       dispatch_once(&Static.token) {           //若对象类型正确,则执行Swizzle方法           if self == SwizzlingDemo.self {               swizzlePrintMethod()           }        }    }}
由于initialize方法会在系统加载这个类的时候被调用 因此,我们就可以确保我们的代码在最初的时候就可以执行,且仅执行一次 @H_301_190@*注:在OC的开发中,一般是在load方法中执行Swizzling的 @H_301_190@但Swift从1.2版本之后就不再支持load方法,因此只能重写initialize方法
这样,我们就通过extension完成了一次完整的Swizzling 当我们在程序中任何其他地方调用这个对象的printA方法时,控制台打印的将始终是B
3、使用Method Swizzling解决问题: 那么回到我们最初的问题 如果我们想要在不修改UIFont和UILabel代码的前提下加入我们想要的 *** 作,要如何做呢? 代码实现的基本原理还是一样的,为了简单起见,在此只以UILabel的awakeFromNib方法为例
首先,我们需要在UILabel中,新定义一个可以修改系统默认字体的方法:
// MARK: - New Font Methodsextension UILabel {   //方法名随意,但由于准备替换掉系统的awakeFromNib方法,因此如此命名   @objc func myAwakeFromNib() {             //仅替换掉系统默认字体,不修改其他字体       if self.Font.FontDescriptor().postscriptname == ".SFUIText-Regular" {           let Font = UIFont(name: "FZLanTingKanHei-R-GBK",size: self.Font.pointSize)           self.Font = Font       }    }}
然后,我们需要一段代码,使用自定义的方法替换掉系统的awakeFromNib方法:
extension UILabel {    private class func swizzleSystemLabel() {       //获取两个awakeFromNib方法的声明       let systemAwakeFromNibSelector = #selector(UILabel.awakeFromNib)       let myAwakeFromNibSelector = #selector(UILabel.myAwakeFromNib)       //获取两个awakeFromNib方法的实现       let systemAwakeFromNibMethod = class_getInstanceMethod(self,systemAwakeFromNibSelector)       let myAwakeFromNibMethod = class_getInstanceMethod(self,myAwakeFromNibSelector)              //交换两个方法的实现       method_exchangeImplementations(systemAwakeFromNibMethod,myAwakeFromNibMethod)   }}
最后,我们需要重写UILabel的initialize方法,确保我们的swizzle方法被执行:
extension UILabel {    overrIDe public class func initialize() {        struct Static {            staticvar token: dispatch_once_t =0        }                dispatch_once(&Static.token) {            if self == UILabel.self {                swizzleSystemLabel()            }        }    }}
这样,当UILabel从xib文件中被加载的时候,系统会自动调用UILabel的awakeFromNib方法 但由于这个方法的实现已经被我们替换了,因此实际上执行的就是myAwakeFromNib方法的实现 从而用我们自定义的字体,来替换掉系统的默认字体了
4、使用Method Swizzling的注意事项: 由于Method Swizzling本质上是直接替换了两个方法的实现 因此,如果我们替换了一些有系统默认实现的方法(例如init方法),那要如何保证原有的实现也能够被调用呢?
答案并不难:在自定义方法的实现中,调用系统原有方法的实现就可以了 但是!(注意但是!)让我们看下面两个例子,想想到底哪个是正确的:
例子1:
@objc func myAwakeFromNib() {   self.awakeFromNib()          //调用awakeFromNib方法   ...}
例子2:
@objc func myAwakeFromNib() {   self.myAwakeFromNib()       //调用myAwakeFromNib方法   ...}
乍一看,似乎第一个例子是正确的,因为调用的就是系统原有的awakeFromNib方法嘛 但是,再仔细想一想,这个 方法体应该是什么时候,通过 哪个方法名被调用的呢? 答案就是,在程序运行的时候,通过awakeFromNib方法名被调用的
所以,正确答案应该是例子2 虽然看上去像是递归,实际上只是 貌合神离罢了 虽然现在看上去像是在一起,但只要程序一运行,当方法交换完成后,这个方法体就不再属于这个方法名了
在这个时候,myAwakeFromNib的方法体,事实上已经属于awakeFromNib这个方法名了 而系统原有的awakeFromNib这个方法的实现,则已经到了myAwakeFromNib这个方法名下了
三、关联对象:Associated Objects 1、为什么要用Associated Objects 如果我们想要往一个类中增加属性,通常情况下可以使用继承的方法 例如,如果我们想要往UIImage中增加一个通用的属性category 那就可以先定义一个MYIMage,使其继承自UIImage 然后向其中增加category属性,并且在程序的所有地方使用MYImage就可以了
但是,这样的做法,会带来几个问题: 1、在程序中所有应当使用UIImage的地方,都必须使用MYImage,整个程序的可读性变差,并且很容易因为疏忽而出错 2、在xib或storyboard中拖入控件的时候,必须要将其类型设置为MYImage,进一步增加出错的可能性 3、最关键的是,往往我们是在开发过程中,才想到加上这个属性,这样就需要全局大改一次代码
因此,如果我们想要在不改动源代码和不使用子类的情况下,想要往类中增加属性,该怎么办呢? 答案就是使用Associated Objects
2、什么是Associated Objects: 简单理解,Associated Objects就是在不修改原始代码的情况下,为类增加额外的属性
首先以上面的场景为例,若我们在extension中直接增加为UIImage增加category属性,编译器会直接报错 错误的原因很明显,在extension中不允许声明可以保存数据的变量
那么,如果我们想要让其编译通过,就把这个属性声明为getter和setter,如下:
extension UIImage {    var category: String? {        get {}        set {}    }}
看起来好像只是比上面多了两个方法而已,为什么就不报错了呢? 是因为这样写并不等同于定义category变量,而是等同于定义了两个方法:
func getcategory () -> String {}func setcategory (newValue: String) {}
这样,我们就可以通过UIImage.category设置和获取其属性了。 但是!此时的get和set方法还并没有实现,也就是说如果现在我们就调用这个属性,会直接崩溃报错 那么,我们要如何实现这两个方法呢?
我们知道,set方法的含义,就是将传入的值保存起来,而get方法的含义,就是将保存的值取出来 所以,现在的问题就变成了,当我们实现set方法的时候,我们需要把传入的值保存到哪里 以及当我们实现get方法的时候,我们需要从哪里取出这个值 这就要使用到Associated Objects了
下面的代码即是完整的Associated Objects实现:
extension UIImage {    //定义属性的指针    private struct AssociatedKeys {        static var categoryPointer = "demo_categoryPointer"    }       var category: String? {        get {            //根据指针从内存中取出变量的值            return objc_getAssociatedobject(self,&AssociatedKeys.categoryPointer) as? String        }        set {            //将传入的值保存到指针所对应的地址中            if let newValue = newValue {                objc_setAssociatedobject(self,&AssociatedKeys.categoryPointer,newValue as String?,.OBJC_ASSOCIATION_RETAIN_NONATOMIC                )            }        }    }}
在上面的例子中,可以看到使用了objc_getAssociatedobject和objc_setAssociatedobject两个方法 这两个方法即是Associated Objects的核心,它们的原理是先定义一个指针,然后通过这个指针的地址来存取变量
当按照以上的方式填写好之后,我们就完全正常的使用UIImage的category属性了,就好像这个属性是原生的那样
3、如何使用Associated Objects: 使用Associated Objects,主要分为三步: 1、定义属性的指针:
private struct AssociatedKeys {    static var categoryPointer = "demo_categoryPointer"}
在这一步中,任何名称都可以任意修改,包括struct的名称,变量的名称和对应的字符串 但是需要注意的是,这里的字符串才代表着指针的名称,而不是这里的变量
也就是说,当我们需要存取category这个属性值的时候 是根据 "demo_categoryPointer 这个指针的地址来存取变量的,而不是根据 categoryPointer来存取变量的 因此,我们在定义这个字符串的时候,为了避免和系统原有指针发生冲突,最好在前面加上【xx_】这样的格式
2、定义get方法:
get {     return objc_getAssociatedobject(self,&AssociatedKeys.categoryPointer) as? String}
这个方法很好理解,第二个参数 & AssociatedKeys .categoryPointer对应的就是变量保存的地址 在这里,如果我们要定义其他类型的变量,则把as后面的String改成对应类型的变量就可以了
3、定义set方法:
set {     //将传入的值保存到指针所对应的地址中     if let newValue = newValue {         objc_setAssociatedobject(self,.OBJC_ASSOCIATION_RETAIN_NONATOMIC         )     }}
在这里,我们需要注意的是最后一个参数: . OBJC_ASSOCIATION_RETAIN_NONATOMIC 这个参数的含义表示了该地址占用的内存,应当在什么情况下被释放,以及是否线程安全
该参数共有5种类型,分别如下:
case OBJC_ASSOCIATION_ASSIGNcase OBJC_ASSOCIATION_RETAIN_NONATOMICcase OBJC_ASSOCIATION_copY_NONATOMICcase OBJC_ASSOCIATION_RETAINcase OBJC_ASSOCIATION_copY
乍看上去似乎很复杂,但现在还不是理解它们的时候 要想理解这5种类型,首先要理解在OC中,内存的分配和自动回收(ARC)的机制
在OC中,当一个变量想要存取值的时候,本质上是从这个变量的地址 指向的内存中进行读取的 而想要让一个变量指向一块内存,主要有两种方式: 第一种:创建一个变量并申请一块内存,使这个变量的地址指向这块内存 第二种:将一个变量已经指向的一块内存,赋值给另一个变量
好,OC的内存分配机制就讲到这里,下面再讲一下ARC的机制: 当某块内存被分配给变量时,ARC会自动为这块内存维护一个引用计数 当有变量 引用这块内存的时候,这块内存的引用计数+1 当引用这块内存的变量被销毁的时候,这块内存的引用计数-1 当某块内存的引用计数为0的时候,这块内存被释放
以上两个机制看起来似乎都很好理解,那么问题来了 在内存分配中,一个变量是“ 指向”一块内存的 而在ARC中,一个变量则是“ 引用”一块内存的 那么这里的“指向”和“引用”是什么关系呢?
让我们把上面两种关系合起来,答案就很清楚了: 第一种:当我们创建一个变量并申请一块内存,同时让这个变量指向这块内存的时候,该变量是否引用了这块内存? 答案是肯定的,这个变量肯定引用了这块内存。因为这块内存刚刚被创建,引用计数还是0 如果该变量没有引用这块内存的话,这块内存很快就会被回收,那创建这个变量就毫无意义了
第二种:当我们将一个变量指向的内存赋值给另一个变量时,被赋值的变量是否引用了这块内存? 这个问题的答案,就要根据赋值的方式而定了,可以分为以下三种情况: *注:为了简单起见,将这块内存称之为内存A,将被赋值的变量称为变量B 方式一:仅让变量B指向内存A,但不持有内存A的引用 方式二:让变量B指向内存A,同时持有内存A的引用 方式三:重新申请一块内存B,复制内存A的内容,并让变量B指向内存B
以上三种方式中,当变量A被释放后,变量B的值就会有所不同 方式一:当变量A被释放后,内存A的引用计数变为0,内存A释放,变量B的值自动变为nil 方式二:当变量A被释放后,变量B还持有内存A的引用,内存A不释放,通过变量B还可以正常访问内存A 方式三:当变量A被释放后,内存A的引用计数变为0,内存A释放,但通过变量B还可以正常访问内存B
以上三种方式是不是都很好理解?以及,是不是觉得【方式一】、【方式二】、【方式三】这三种叫法实在太难听了? 其实,在OC中,对于以上三种方式的赋值,是有相应的名字的: 方式一:assign 方式二:retain 方式三:copy
好,下面再返回来看set方法最后一个参数的5种类型:
case OBJC_ASSOCIATION_ASSIGNcase OBJC_ASSOCIATION_RETAIN_NONATOMICcase OBJC_ASSOCIATION_copY_NONATOMICcase OBJC_ASSOCIATION_RETAINcase OBJC_ASSOCIATION_copY
除了最后一个NONATOMIC单词还不认识之外,其他的是不是都能看明白了呢?
至于NONATOMIC这个单词,主要控制的是该变量是否线程安全 所谓的线程安全就是,是否能有多个线程同时访问并修改这个变量所指向的内存 如果名称中包含NONATOMIC,则说明该变量所指向的内存可以被多个线程同时访问(即线程不安全),反之,则不可以同时访问(线程安全)
由于线程安全会导致运行时效率略有降低,并且在通常情况下我们很少会多线程访问一个变量 因此在不确定选择哪个的情况下,直接使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC即可
4、使用Associated Objects的注意事项: 最重要的一点就是: 不要滥用 任何技术都是这样,除非你确定为什么要使用,并且使用这项技术是最佳解决方案的时候,再去使用它 因此,仅当你想要从架构的层面上为类增加某个属性的时候,并且这个属性的作用十分通用和明确,才使用Associated Objects
资料来源: Runtime详解: http://nshipster.cn/swift-objc-runtime/ 使用Runtime全局修改UILabel的默认字体: http://my.oschina.net/u/2340880/blog/538356 使用Runtime全局修改UIFont的默认字体: https://gist.github.com/feighter09/721c270897efb9b7381d(注意有错误:未调用dispatch_once) 关联对象的五种类型详解: http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/ 总结

以上是内存溢出为你收集整理的【Swift】Swift黑魔法 - Runtime全部内容,希望文章能够帮你解决【Swift】Swift黑魔法 - Runtime所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1076293.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-26
下一篇 2022-05-26

发表评论

登录后才能评论

评论列表(0条)

保存