一、什么是Runtime:
Runtime是苹果开发中比extension更加强大的一项黑科技
*Runtime在OC和Swift中都可以使用
二、方法交叉:Method Swizzling
1、为什么要用Method Swizzling
在实际应用中,我们可能会遇到这样的场景:
我们定义了很多Label控件,有些是在xib中定义的,有些是通过代码创建的
在这些Label中,有些使用的是系统默认字体,有些使用的则是自定义字体
然后有一天,设计师突然给了一个新字体,说所有的系统默认字体都应该替换成这个字体
虽然很不爽,但还是花了半天时间把系统中所有的Label字体都修改了一遍(可能有些还有遗漏)
然后又有一天,老板说这个字体不好看!给我换成另外一个字体!于是设计师又给了一个新字体
于是又花了半天时间把所有Label的字体再修改一遍,并且出现了更多的遗漏
就这样,我们浪费了很多时间在改字体上面,最后出来的效果还不尽人意
于是我们就心想,如果有一个方法,能够把所有Label的系统默认字体,都替换成我们想要的新字体该多好啊
这样,不管我们在xib中定义Label还是在代码中创建Label,都直接使用系统默认字体即可
当然,这个需求是可以实现的,但首先要有解决问题的思路:
思路一:每当程序需要获取系统默认字体时,直接返回我们想要的字体
思路二:每当Label被创建或加载时,判断当前Label的字体是否为系统字体,若是,则修改为我们想要的字体
这就要用到Runtime中的Method Swizzling机制了
2、什么是Method Swizzling:
当然,我们也可以直接提供给系统一个Selector,系统也可以通过该Selector找到对应的Method
为了理解这个概念,我们首先看看下面这个类:
class SwizzlingDemo { func printA() { print("A") } func printB () { print("B") } }
很明显:
当我们调用printA()的时候,控制台就会打印A
当我们调用printB()的时候,控制台就会打印B
在调用printA()的时候,让控制台打印B
而调用printB()的时候,让控制台打印A呢?
这样就需要用到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()
从代码中可以很清晰地看出:
再根据两个Selector取出了两个方法的Method
最后交换了两个方法的Method,Swizzing完毕。
是的,就是这么简单。
系统实际上执行的就是printB的Method,也就是在控制台打印B了
一个最推荐(也是最安全)的做法是:
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() } } } }
因此,我们就可以确保我们的代码在最初的时候就可以执行,且仅执行一次
*注:在OC的开发中,一般是在load方法中执行Swizzling的
这样,我们就通过extension完成了一次完整的Swizzling
3、使用Method Swizzling解决问题:
那么回到我们最初的问题
// MARK: - New Font Methods extension 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 } } }
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) } }
extension UILabel { override public class func initialize() { struct Static { staticvar token: dispatch_once_t =0 } dispatch_once(&Static.token) { if self == UILabel.self { swizzleSystemLabel() } } } }
从而用我们自定义的字体,来替换掉系统的默认字体了
4、使用Method Swizzling的注意事项:
由于Method Swizzling本质上是直接替换了两个方法的实现
但是!(注意但是!)让我们看下面两个例子,想想到底哪个是正确的:
例子1:
例子2:
所以,正确答案应该是例子2
虽然看上去像是递归,实际上只是
貌合神离罢了
三、关联对象:Associated Objects
1、为什么要用Associated Objects
那就可以先定义一个MYIMage,使其继承自UIImage
但是,这样的做法,会带来几个问题:
1、在程序中所有应当使用UIImage的地方,都必须使用MYImage,整个程序的可读性变差,并且很容易因为疏忽而出错
2、在xib或storyboard中拖入控件的时候,必须要将其类型设置为MYImage,进一步增加出错的可能性
答案就是使用Associated Objects
2、什么是Associated Objects:
错误的原因很明显,在extension中不允许声明可以保存数据的变量
那么,如果我们想要让其编译通过,就把这个属性声明为getter和setter,如下:
extension UIImage { var category: String? { get {} set {} } }
看起来好像只是比上面多了两个方法而已,为什么就不报错了呢?
是因为这样写并不等同于定义category变量,而是等同于定义了两个方法:
func getCategory () -> String {} func setCategory (newValue: String) {}
那么,我们要如何实现这两个方法呢?
所以,现在的问题就变成了,当我们实现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的核心,它们的原理是先定义一个指针,然后通过这个指针的地址来存取变量
3、如何使用Associated Objects:
使用Associated Objects,主要分为三步:
1、定义属性的指针:
private struct AssociatedKeys { static var categoryPointer = "demo_categoryPointer" }在这一步中,任何名称都可以任意修改,包括struct的名称,变量的名称和对应的字符串
但是需要注意的是,这里的字符串才代表着指针的名称,而不是这里的变量
也就是说,当我们需要存取category这个属性值的时候
是根据
"demo_categoryPointer
”这个指针的地址来存取变量的,而不是根据
categoryPointer来存取变量的
因此,我们在定义这个字符串的时候,为了避免和系统原有指针发生冲突,最好在前面加上【xx_】这样的格式
2、定义get方法:
@H_403_469@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_ASSIGN case OBJC_ASSOCIATION_RETAIN_NONATOMIC case OBJC_ASSOCIATION_COPY_NONATOMIC case OBJC_ASSOCIATION_RETAIN case 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_ASSIGN case OBJC_ASSOCIATION_RETAIN_NONATOMIC case OBJC_ASSOCIATION_COPY_NONATOMIC case OBJC_ASSOCIATION_RETAIN case OBJC_ASSOCIATION_COPY
除了最后一个NONATOMIC单词还不认识之外,其他的是不是都能看明白了呢?
至于NONATOMIC这个单词,主要控制的是该变量是否线程安全
所谓的线程安全就是,是否能有多个线程同时访问并修改这个变量所指向的内存
如果名称中包含NONATOMIC,则说明该变量所指向的内存可以被多个线程同时访问(即线程不安全),反之,则不可以同时访问(线程安全)
由于线程安全会导致运行时效率略有降低,并且在通常情况下我们很少会多线程访问一个变量
因此在不确定选择哪个的情况下,直接使用
OBJC_ASSOCIATION_RETAIN_NONATOMIC即可
4、使用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)
原文链接:https://www.f2er.com/swift/323799.html