本文主要是介绍24、OC语言的动态性学习(Runtime),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 一、动态类型
- 二、动态绑定
- 三、动态加载
- 四、消息机制的基本原理
- 消息查找阶段
- 动态方法解析
- 消息转发
- 消息快转发
- 消息慢转发
- 消息发送以及转发机制的总结
- 重写respondsToSelector方法
- 代码实现
- 动态解析
- 快速转发
- 慢速转发
- 五、Method Swizzling(动态方法交换)
- 简介
- 使用Method Swizzling需要的几个方法
- Method Swizzling 使用方法
- Method Swizzling 简单使用
- Method Swizzling 方案一
- Method Swizzling 方案 B
- Method Swizzling 方案 C
- Method Swizzling 方案 D
- Method Swizzling 使用注意
- 1、应该只在 `+load` 中执行 `Method Swizzling`。
- 2. `Method Swizzling` 在 `+load` 中执行时,不要调用 `[super load];`。
- 3. `Method Swizzling` 应该总是在 `dispatch_once` 中执行。
- 4. 使用 `Method Swizzling` 后要记得调用原生方法的实现。
- 5. 避免命名冲突和参数 _cmd 被篡改。
- 6. 谨慎对待 Method Swizzling。
- 7. 对于 Method Swizzling 来说,调用顺序 很重要。
- 8. 在Swift中load方法和initialize方法已经不允许使用了
- 在Swift中如何使用Runtime
- @objc、@objcMembers和dynamic
- 在桥接文件中导入runtime头文件
- Swift中实现Method Swizzling遇到的问题
- Swift中实战Method Swizzling
- 方法一
- 方法二
- Method Swizzling 应用场景
- 全局页面统计功能
- 字体根据屏幕尺寸适配
- 处理按钮重复点击
- TableView、CollectionView 异常加载占位图
- APM(应用性能管理)、防止程序崩溃
- 拦截点击事件
- 简介
- 给所有的View事件添加isNeedCheckLogin属性
- 拦截UIControl控件的点击事件
- 拦截UITableView的点击事件
- 拦截UICollectionView的点击事件
- 拦截手势的点击事件
- 使用过程中遇到的问题
- 六、获取类详细属性、方法
- 简述
- 获取类详细属性、方法
- 获取类的成员变量列表
- 获取类的属性列表
- 获取类的方法列表
- 获取类所遵循的协议列表
- 应用场景
- 修改私有属性
- 万能控制器跳转
- 实现字典转模型
- 创建模型
- 在 NSObject 分类中实现字典转模型
- 测试代码
- 改进 iOS 归档和解档
- 七、关联对象(Associated Object)
- 什么是关联对象
- 关联对象
- 如何关联对象
- 关联对象:为分类添加“属性”
- 分类的限制
- 用法解析
- 实战场景
- 利用关联属性实现导航栏渐变
- UIImageView
- 为UI控件关联事件Block体
- UIAlertView
- UIButton
- 关联对象:关联观察者对象
- 关联对象:为了不重复执行
- 八、数据结构
- objc_object
- objc_class
- isa
- cache_t
- class_data_bits_t
- mehod_t
- Type Encodings
- 整体数据结构
- 类对象与元类对象
- 类对象和元类对象有什么区别和联系?
- 经典面试题
- 编译时:即编译器对语言的编译阶段。编译时只是对语言进行最基本的检查报错,包括词法分析、语法分析等等,将程序代码翻译成计算机能够识别的语言(例如汇编等),编译通过并不意味着程序就可以成功运行。
- 运行时:即程序通过了编译这一关之后,编译好的代码会被装载到内存中运行起来的阶段,这个时候会具体对类型进行检查,而不仅仅是对代码的简单扫描分析,此时若出错程序会崩溃。
OC语言的动态性主要体现在三个方面:动态类型(Dynamic typing)、动态绑定(Dynamic binding)和动态加载(Dynamic loading)。
一、动态类型
动态类型就是指运行时再去决定对象的类型,简单来说就是id类型。id类型是通用的对象类型,任何对象都可以被id指针所指。使用id类型将对象的类型推迟到运行时才确定,由赋给它的对象类型决定该对象类型。
也就是说id修饰的对象是动态类型对象,其他在编译期指明类型的为静态类型对象。
所以开发中如果不是涉及到多态,尽量还是使用静态的类型,这样编写错误,编译器会提前查出问题,可读性更高一点。
// 编译时认为是NSString,这是赋值了一个NSData对象编译器会给出警告信息:Incompatible pointer types initializing 'NSString *' with an expression of type 'NSData *'
NSString *testObject = [[NSData alloc]init];
// 编译其认为是NSString,所以允许使用NSString的方法,不会有警告和错误,
[testObject stringByAppendingString:@"string"];
// 编译期不允许使用NSData的方法,错误提示;No visible @interface for 'NSString' declares the selector 'base64EncodedDataWithOptions:'
[testObject base64EncodedDataWithOptions:NSDataBase64Encoding64CharacterLineLength];
如以上代码,testObject
在编译时,指针的类型是NSString
,也就是说编译时期是被当做一个NSString
类型来处理,编译器在类型检查时发现类型不匹配会给出警告信息,testObject
在运行时,指针指向的是一个NSData
对象,因此如果指针调用了NSString
的方法,那么虽然编译通过了,但运行时会出现崩溃,
在实际开发中,可以通过-isKindOfClass:
方法来确定对象的具体类型,在确定对象为某类成员后,可以安全地进行强制转换,继续之后的工作。
-isKindOfClass:
和-isMemberOfClass:
都是NSObject
的方法,-isKindOfClass:
可以用来确定某个对象是否是某个类或者其子类的实例对象,而isKindOfClass:
用以确定某个对象是否某个类的实例对象。
id obj = someInstance;
if ([obj isKindOfClass:someClass])
{someClass *classSpecifiedInstance = (someClass *)obj;// Do Something to classSpecifiedInstance which now is an instance of someClass//...
}
二、动态绑定
基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。
由于OC的动态特性,在OC中其实很少提及“函数”的概念,传统的函数一般在编译时就已经把参数信息和函数实现打包到编译后的源码中了,而在OC中最常使用的是消息机制。调用一个实例的方法,所做的是向该实例的指针发送消息,实例在收到消息后,从自身的实现中寻找响应这条消息的方法。
动态绑定所做的就是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。 这里所指的属性和方法当然包括了原来没有在类中实现的,而是在运行时才需要的新加入的实现。在Cocoa层,我们一般向一个NSObject
对象发送-respondsToSelector:
或者-instancesRespondToSelector:
等来确定对象是否可以对某个SEL做出响应,而在OC消息转发机制被触发之前,对应的类的+resolveClassMethod:
和+resolveInstanceMethod:
将会被调用,在此时有机会动态地向类或者实例添加新的方法,也即类的实现是可以动态绑定的。
void dynamicMethodIMP(id self, SEL _cmd)
{// implementation ....
}//该方法在OC消息转发生效前被调用
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{ if (aSEL == @selector(resolveThisMethodDynamically)) {//向[self class]中新加入返回为void的实现,SEL名字为aSEL,实现的具体内容为dynamicMethodIMP class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, “v@:”);return YES;}return [super resolveInstanceMethod:aSel];
}
当然也可以在任意需要的地方调用class_addMethod
或者method_setImplementation
(前者添加实现,后者替换实现),来完成动态绑定的需求。
基本的动态特性在常规的Cocoa开发中非常常用,特别是动态类型和动态绑定。由于Cocoa程序大量地使用Protocol-Delegate的设计模式,因此绝大部分的delegate指针类型必须是id,以满足运行时delegate的动态替换。
三、动态加载
动态加载分为两部分:动态资源的加载(如:图片资源),代码模块的加载;这些都是在运行时根据需要有选择性的添加到程序中的,是一种代码和资源的“懒加载”模式,这样降低编译时期对内存的开销,提供程序的性能。
如:资源在动态加载图片进行屏幕适配时,因为同一个图片对象可能会准备几种不同分辨率的图片资源,程序就会根据当前机型动态的选择对应分辨率的图片,如:@1x,@2x,@3x的。
四、消息机制的基本原理
Objective-C 语言中,对象方法调用都是类似 [receiver selector];
的形式,其本质就是让对象在运行时发送消息的过程。OC中的方法的调用时通过objc_msgSend(或者objc_msgSendSuper,或者objc_msgSend_stret,或者objc_msgSendSuper_stret)函数,向调用者发送名为SEL的消息,找到具体的函数地址IMP,进而执行该函数。
-
编译阶段:
[receiver selector];
方法会被编译器转换为:
objc_msgSend(receiver,selector)
(不带参数)
objc_msgSend(recevier,selector,org1,org2,…)
(带参数) -
运行时阶段:消息接受者receiver寻找对应的selector。
objc_msgSend
的执行流程可以分为3大阶段:
- 1、消息查找
- 2、动态方法解析
- 3、消息转发
消息查找阶段
1、通过recevier的isa指针找到recevier的Class(类);根据给定的SEL,到缓存的方法列表cache_t
中通过哈希查找得到对应的bucket_t在数组中的位置,就可以提取到它所对应的IMP函数指针,返回给调用方调用即可。
如果在当前类的Class(类)中没有找到这个selector,就继续在它的 superClass(父类)中寻找;
通过当前类的superClass
来访问它的父类,需要判断父类是否为空,当前类如果是NSObject
,那么它的父类就是nil,就可以结束父类逐级查找过程了。如果不是nil,就到父类的缓存中去查找,如果在缓存中查到了,就结束父类逐级查找的流程。如果没有找到就需要遍历当前类的父类的方法列表是否有实现,如果没有继续逐级向上查找,直到NSObject,还没有找到就结束父类逐级查找。
这个时候就会触发动态方法解析。
动态方法解析
Objective-C
运行时会调用 +resolveInstanceMethod:
或者 +resolveClassMethod:
,让你有机会提供一个函数实现。我们可以通过重写这两个方法,通过class_addMethod
添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。
主要用的的方法如下:
// 类方法未找到时调起,可以在此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;/** * class_addMethod 给类添加一个新方法和该方法的实现* @param cls 将要添加方法的类* @param name 将要添加的方法名* @param imp 实现这个方法的指针* @param types imp 要添加的方法的返回值和参数* @return 如果添加方法成功返回 YES,否则返回 NO*/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char * _Nullable types);
举个例子:
#import "ViewController.h"
#include "objc/runtime.h"@interface ViewController ()@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// 执行 fun 函数[self performSelector:@selector(fun)];
}// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(fun)) { // 如果是执行 fun 函数,就动态解析,指定新的 IMPclass_addMethod([self class], sel, (IMP)funMethod, "v@:");return YES;}return [super resolveInstanceMethod:sel];
}void funMethod(id obj, SEL _cmd) {NSLog(@"funMethod"); //新的 fun 函数
}@end
打印结果:
2019-06-12 10:25:39.848260+0800 runtime[14884:7977579] funMethod
v@:表示返回值和参数,可以在苹果官网查看Type Encoding相关文档 https://developer.apple.com/library/mac/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
面试的过程中,经常会问到你是否使用过performSelector。
performSelector的应用场景之一就是一个类在编译时没有某个方法,在有运行时才会添加某个方法的实现。就需要使用performSelector去调用这个类的方法。
主要是为了考察动态添加方法。
消息转发
消息快转发
如果上一步中 +resolveInstanceMethod:
或者 +resolveClassMethod:
没有添加其他函数实现,运行时就会进行下一步:消息转发。消息转发可以分为消息快转发和消息慢转发,消息快转发其实就是重定向,把消息转交给其他类处理。
-forwardingTargetForSelector:
对应的就是消息的快速转发流程,它主要是返回一个新的receiver,去处理sel这个当前类无法处理的消息,如果处理不了,就会转到效率低下的forwardInvocation:
。
在效率方面,forwardingTargetForSelector:
领先forwardInvocation:
一个数量级,因此,最好不要用后者的方式处理消息的转发逻辑。
关于forwardingTargetForSelector:
返回新的receiver,需要注意以下几点:
- 绝对不能返回self,否则会陷入无限循环。
- 不处理的话,可以返回nil,或者
[super forwardingTargetForSelector:sel]
(非根类的情况),此时会走methodSignatureForSelector:
慢转发流程; - 如果有这个receiver,此时相当于执行
objc_msgSend(newReceiver,sel,...)
,那么它必须拥有和被调用的方法相同方法前面的方法(方法名、参数列表、返回值类型都必须一致)。
用到的方法:
// 重定向方法的消息接收者,返回一个类或实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector;
注意:这里+resolveInstanceMethod:
或者 +resolveClassMethod:
无论是返回 YES,还是返回 NO,只要其中没有添加其他函数实现,运行时都会进行下一步。
举个例子:
#import "ViewController.h"
#include "objc/runtime.h"@interface Person : NSObject- (void)fun;@end@implementation Person- (void)fun {NSLog(@"fun");
}@end@interface ViewController ()@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// 执行 fun 方法[self performSelector:@selector(fun)];
}+ (BOOL)resolveInstanceMethod:(SEL)sel {return YES; // 为了进行下一步 消息接受者重定向
}// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {if (aSelector == @selector(fun)) {return [[Person alloc] init];// 返回 Person 对象,让 Person 对象接收这个消息}return [super forwardingTargetForSelector:aSelector];
}
打印结果:
2019-06-12 17:34:05.027800+0800 runtime[19495:8232512] fun
我们通过 forwardingTargetForSelector
可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil
,也不是 self
,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息慢转发。
消息慢转发
如果经过消息动态解析、消息快转发,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector:
方法获取函数的参数和返回值类型。
- 如果
-methodSignatureForSelector:
返回了一个NSMethodSignature
对象(函数签名),Runtime 系统就会创建一个NSInvocation
对象,并通过-forwardInvocation:
消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。 - 如果
-methodSignatureForSelector:
返回 nil。则 Runtime 系统会发出-doesNotRecognizeSelector:
消息,程序也就崩溃了。
所以我们可以在-forwardInvocation:
方法中对消息进行转发。
用到的方法:
// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
举个例子:
#import "ViewController.h"
#include "objc/runtime.h"@interface Person : NSObject- (void)fun;@end@implementation Person- (void)fun {NSLog(@"fun");
}@end@interface ViewController ()@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// 执行 fun 函数[self performSelector:@selector(fun)];
}+ (BOOL)resolveInstanceMethod:(SEL)sel {return YES; // 为了进行下一步 消息接受者重定向
}- (id)forwardingTargetForSelector:(SEL)aSelector {return nil; // 为了进行下一步 消息重定向
}// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {return [NSMethodSignature signatureWithObjCTypes:"v@:"];}return [super methodSignatureForSelector:aSelector];
}//
- (void)forwardInvocation:(NSInvocation *)anInvocation {SEL sel = anInvocation.selector; // 从 anInvocation 中获取消息Person *p = [[Person alloc] init];if([p respondsToSelector:sel]) { // 判断 Person 对象方法是否可以响应 sel[anInvocation invokeWithTarget:p]; // 若可以响应,则将消息转发给其他对象处理} else {[self doesNotRecognizeSelector:sel]; // 若仍然无法响应,则报错:找不到响应方法}
}
@end
打印结果:
2019-06-13 13:23:06.935624+0800 runtime[30032:8724248] fun
可以看到,我们在 -forwardInvocation:
方法里面让 Person 对象去执行了 fun 函数。
既然 -forwardingTargetForSelector:
和 -forwardInvocation:
都可以将消息转发给其他对象处理,那么两者的区别在哪?
区别就在于 -forwardingTargetForSelector:
只能将消息转发给一个对象。而 -forwardInvocation:
可以将消息转发给多个对象。
以上就是 Runtime 消息转发的整个流程。
消息发送以及转发机制的总结
调用 [receiver selector];
后,进行的流程:
-
1、编译阶段:
[receiver selector];
方法被编译器转换为:
objc_msgSend(receiver,selector)
(不带参数)
objc_msgSend(recevier,selector,org1,org2,…)
(带参数) -
2、运行时阶段:消息接受者recever寻找对应的selector。
(1)通过recevier的isa指针找到recevier的class(类);
(2)在class(类)的method list(方法列表)中找对应的 selector;
(3)如果在class(类)中没有找到这个selector,就继续在它的 superclass(父类)中寻找;
(4)一旦找到对应的selector,直接执行recever对应selector方法实现的 IMP(方法实现)。
(5)若找不到对应的selector,Runtime系统进入消息转发机制。 -
3、运行时消息转发阶段:
(1)动态方法解析:通过重写+resolveInstanceMethod:
或者+resolveClassMethod:
方法,利用class_addMethod
方法添加其他函数实现;
(2)消息快速转发:如果上一步添加其他函数实现,可在当前对象中利用-forwardingTargetForSelector:
方法将消息的接受者转发给其他对象;
(3)消息慢速转发:如果上一步没有返回值为 nil,则利用-methodSignatureForSelector:
方法获取函数的参数和返回值类型。- 如果
-methodSignatureForSelector:
返回了一个NSMethodSignature
对象(函数签名),Runtime 系统就会创建一个NSInvocation
对象,并通过-forwardInvocation:
消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。 - 如果
-methodSignatureForSelector:
返回 nil。则 Runtime 系统会发出-doesNotRecognizeSelector:
消息,程序也就崩溃了。
- 如果
重写respondsToSelector方法
OC中respondsToSelector方法可以用检查类对象是否能够处理对应的selector,当我们通过消息转发机制来处理selector时,respondsToSelector并不能按原意正常工作了,这个时候需要重写respondsToSelector方法,用来告诉方法调用者对应的selector是能够被处理的。如果是在动态解析阶段使用class_addMethod来为类动态添加方法,则不需要重写respondsToSelector.
代码实现
动态解析
void printTest() {NSLog(@"printTest");
}@interface Test : NSObject
- (void) print;
@end@implementation Test+ (BOOL)resolveInstanceMethod:(SEL)sel {if(sel_isEqual(sel, @selector(print))) {//如果是print方法 就把printTest方法的实现地址链接到print上面class_addMethod([self class], @selector(print), (IMP)printTest, "v@:");return YES;}return [super resolveInstanceMethod:sel];
}@end@interface ViewController ()
@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];Test* tt = [[Test alloc] init];if([tt respondsToSelector:@selector(print)]){[tt print];}
}
@end
快速转发
@interface Test2:NSObject- (void) print;
@end@implementation Test2
- (void) print {NSLog(@"Test2");
}
@end@interface Test : NSObject {Test2* _helper;
}
- (void) print;
- (void) moya;
@end@implementation Test- (instancetype)init
{self = [super init];if (self) {_helper = [[Test2 alloc] init];}return self;
}- (BOOL)respondsToSelector:(SEL)aSelector {if(sel_isEqual(@selector(print), aSelector)){return [_helper respondsToSelector:aSelector];}return [super respondsToSelector:aSelector];
}- (id)forwardingTargetForSelector:(SEL)aSelector {if (sel_isEqual(@selector(print), aSelector)){return _helper;}return [super forwardingTargetForSelector:aSelector];
}- (void) moya {NSLog(@"moya");
}@end@interface ViewController ()
@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];Test* tt = [[Test alloc] init];if([tt respondsToSelector:@selector(print)]){[tt print];}if([tt respondsToSelector:@selector(moya)]){[tt moya];}
}
@end
慢速转发
@interface Test2:NSObject- (void) print;
@end@implementation Test2
- (void) print {NSLog(@"Test2");
}
@end@interface Test : NSObject {Test2* _helper;
}
- (void) print;
- (void) moya;
@end@implementation Test- (instancetype)init
{self = [super init];if (self) {_helper = [[Test2 alloc] init];}return self;
}- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if (sel_isEqual(@selector(print), aSelector)){NSMethodSignature* sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];return sig;}return [super methodSignatureForSelector:aSelector];
}- (void)forwardInvocation:(NSInvocation *)anInvocation {SEL sel = [anInvocation selector];if([_helper respondsToSelector:sel]){[anInvocation invokeWithTarget:_helper];}else{[self doesNotRecognizeSelector:sel];}
}- (BOOL)respondsToSelector:(SEL)aSelector {if (sel_isEqual(@selector(print), aSelector)){return [_helper respondsToSelector:aSelector];}return [super respondsToSelector:aSelector];
}- (void) moya {NSLog(@"moya");
}@end@interface ViewController ()
@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];Test* tt = [[Test alloc] init];if([tt respondsToSelector:@selector(print)]){[tt print];}if([tt respondsToSelector:@selector(moya)]){[tt moya];}
}
@end
五、Method Swizzling(动态方法交换)
简介
Method Swizzling实现将某个类的方法替换成自己定义的类的方法,从而达到Hook的作用,其实质就是交换两个方法的 IMP(方法实现)。
Method Swizzling原理:在OC中,每个类都维护着一个方法(Method)列表,Method则包含SEL和其对应的IMP信息,方法交换做的事情就是把SEL和IMP的对应关系断开,并和新的IMP生成对应关系。
Method
(方法)对应的是 objc_method
结构体;而 objc_method 结构体
中包含了 SEL method_name(方法名)
、IMP method_imp(方法实现)
。
// objc_method 结构体
typedef struct objc_method *Method;struct objc_method {SEL _Nonnull method_name; // 方法名char * _Nullable method_types; // 方法类型IMP _Nonnull method_imp; // 方法实现
};
Method(方法)
、SEL(方法名
)、IMP(方法实现)
三者的关系可以这样来表示:
在运行时,Class(类)
维护了一个 method list(方法列表)
来确定消息的正确发送。method list(方法列表)
存放的元素就是 Method(方法)
。而 Method(方法)
中映射了一对键值对:SEL(方法名):IMP(方法实现)
。
Method swizzling
修改了 method list(方法列表)
,使得不同 Method(方法)
中的键值对发生了交换。比如交换前两个键值对分别为 SEL A : IMP A
、SEL B : IMP B
,交换之后就变为了 SEL A : IMP B
、SEL B : IMP A
。
使用Method Swizzling需要的几个方法
使用Method Swizzling需要用到几个方法:
// 获取一个对象的实例方法
// cls: 方法所在的类
// name: 方法名称的selector
Method class_getInstanceMethod(Class cls, SEL name)// 获取一个类的实例方法
// cls: 方法所在的类
// name: 方法名称的selector
Method class_getClassMethod(Class cls, SEL name)// 交换两个方法
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);/** * class_addMethod 给类添加一个新方法和该方法的实现* @param cls 将要添加方法的类* @param name 将要添加的方法名* @param imp 实现这个方法的指针* @param types imp 要添加的方法的返回值和参数* @return 如果添加方法成功返回 YES,否则返回 NO*/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char * _Nullable types);// 作用: 指定替换方法的实现
// cls : 将要替换方法的类
// name: 将要替换的方法名
// imp: 新方法的指针
// types: 新方法的返回值和参数描述
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) ;// 获取方法的参数和返回值类型描述
const char * _Nullable method_getTypeEncoding(Method _Nonnull m);
Method Swizzling 使用方法
假如当前类中有两个方法:- (void)originalFunction;
和 - (void)swizzledFunction;
。如果我们想要交换两个方法的实现,从而实现调用 - (void)originalFunction;
方法实际上调用的是 - (void)swizzledFunction;
方法,而调用 - (void)swizzledFunction;
方法实际上调用的是 - (void)originalFunction;
方法的效果。那么我们需要像下边代码一样来实现。
Method Swizzling 简单使用
在当前类的 + (void)load;
方法中增加 Method Swizzling
操作,交换 - (void)originalFunction;
和 - (void)swizzledFunction;
的方法实现。
#import "ViewController.h"
#import <objc/runtime.h>@interface ViewController ()@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];[self SwizzlingMethod];[self originalFunction];[self swizzledFunction];
}// 交换 原方法 和 替换方法 的方法实现
- (void)SwizzlingMethod {// 当前类Class class = [self class];// 原方法名 和 替换方法名SEL originalSelector = @selector(originalFunction);SEL swizzledSelector = @selector(swizzledFunction);// 原方法结构体 和 替换方法结构体Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);// 调用交换两个方法的实现method_exchangeImplementations(originalMethod, swizzledMethod);
}// 原始方法
- (void)originalFunction {NSLog(@"originalFunction");
}// 替换方法
- (void)swizzledFunction {NSLog(@"swizzledFunction");
}@end
打印结果:
2019-07-12 09:59:19.672349+0800 Runtime-MethodSwizzling[91009:30112833] swizzledFunction
2019-07-12 09:59:20.414930+0800 Runtime-MethodSwizzling[91009:30112833] originalFunction
一般日常开发中,并不是直接在原有类中进行 Method Swizzling 操作。更多的是为当前类添加一个分类,然后在分类中进行 Method Swizzling 操作。另外真正使用会比上边写的考虑东西要多一点,要复杂一些。
Method Swizzling 方案一
在该类的分类中添加 Method Swizzling
交换方法,用普通方式。
这种方式在开发中应用最多的。但是还是要注意一些事项,我会在接下来的 3. Method Swizzling
使用注意 进行详细说明。
@implementation UIViewController (Swizzling)// 交换 原方法 和 替换方法 的方法实现
+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{// 当前类Class class = [self class];// 原方法名 和 替换方法名SEL originalSelector = @selector(originalFunction);SEL swizzledSelector = @selector(swizzledFunction);// 原方法结构体 和 替换方法结构体Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);/* 如果当前类没有 原方法的 IMP,说明在从父类继承过来的方法实现,* 需要在当前类中添加一个 originalSelector 方法,* 但是用 替换方法 swizzledMethod 去实现它 */BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if (didAddMethod) {// 原方法的 IMP 添加成功后,修改 替换方法的 IMP 为 原始方法的 IMPclass_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));} else {// 添加失败(说明已包含原方法的 IMP),调用交换两个方法的实现method_exchangeImplementations(originalMethod, swizzledMethod);}});
}// 原始方法
- (void)originalFunction {NSLog(@"originalFunction");
}// 替换方法
- (void)swizzledFunction {NSLog(@"swizzledFunction");
}@end
Method Swizzling 方案 B
在该类的分类中添加 Method Swizzling 交换方法,但是使用函数指针的方式。
方案 B 和方案 A 的最大不同之处在于使用了函数指针的方式,使用函数指针最大的好处是可以有效避免命名错误。
#import "UIViewController+PointerSwizzling.h"
#import <objc/runtime.h>typedef IMP *IMPPointer;// 交换方法函数
static void MethodSwizzle(id self, SEL _cmd, id arg1);
// 原始方法函数指针
static void (*MethodOriginal)(id self, SEL _cmd, id arg1);// 交换方法函数
static void MethodSwizzle(id self, SEL _cmd, id arg1) {// 在这里添加 交换方法的相关代码NSLog(@"swizzledFunc");MethodOriginal(self, _cmd, arg1);
}BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {IMP imp = NULL;Method method = class_getInstanceMethod(class, original);if (method) {const char *type = method_getTypeEncoding(method);imp = class_replaceMethod(class, original, replacement, type);if (!imp) {imp = method_getImplementation(method);}}if (imp && store) { *store = imp; }return (imp != NULL);
}@implementation UIViewController (PointerSwizzling)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{[self swizzle:@selector(originalFunc) with:(IMP)MethodSwizzle store:(IMP *)&MethodOriginal];});
}+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {return class_swizzleMethodAndStore(self, original, replacement, store);
}// 原始方法
- (void)originalFunc {NSLog(@"originalFunc");
}@end
Method Swizzling 方案 C
在其他类中添加 Method Swizzling 交换方法
这种情况一般用的不多,最出名的就是 AFNetworking 中的_AFURLSessionTaskSwizzling 私有类。_AFURLSessionTaskSwizzling 主要解决了 iOS7 和 iOS8 系统上 NSURLSession 差别的处理。让不同系统版本 NSURLSession 版本基本一致。
static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) {Method originalMethod = class_getInstanceMethod(theClass, originalSelector);Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector);method_exchangeImplementations(originalMethod, swizzledMethod);
}static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) {return class_addMethod(theClass, selector, method_getImplementation(method), method_getTypeEncoding(method));
}@interface _AFURLSessionTaskSwizzling : NSObject@end@implementation _AFURLSessionTaskSwizzling+ (void)load {if (NSClassFromString(@"NSURLSessionTask")) {NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic popIMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));Class currentClass = [localDataTask class];while (class_getInstanceMethod(currentClass, @selector(resume))) {Class superClass = [currentClass superclass];IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));if (classResumeIMP != superclassResumeIMP &&originalAFResumeIMP != classResumeIMP) {[self swizzleResumeAndSuspendMethodForClass:currentClass];}currentClass = [currentClass superclass];}[localDataTask cancel];[session finishTasksAndInvalidate];}
}+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));}if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));}
}- (void)af_resume {NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");NSURLSessionTaskState state = [self state];[self af_resume];if (state != NSURLSessionTaskStateRunning) {[[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];}
}- (void)af_suspend {NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");NSURLSessionTaskState state = [self state];[self af_suspend];if (state != NSURLSessionTaskStateSuspended) {[[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidSuspendNotification object:self];}
}
Method Swizzling 方案 D
JRSwizzle 和 RSSwizzle 都是优秀的封装 Method Swizzling 的第三方框架。
1、JRSwizzle 尝试解决在不同平台和系统版本上的 Method Swizzling 与类继承关系的冲突。对各平台低版本系统兼容性较强。JRSwizzle 核心是用到了 method_exchangeImplementations 方法。在健壮性上先做了 class_addMethod 操作。
2、RSSwizzle 主要用到了 class_replaceMethod 方法,避免了子类的替换影响了父类。而且对交换方法过程加了锁,增强了线程安全。它用很复杂的方式解决了 What are the dangers of method swizzling in Objective-C? 中提到的问题。是一种更安全优雅的 Method Swizzling 解决方案。
Method Swizzling 使用注意
Method Swizzling 之所以被大家称为黑魔法,就是因为使用 Method Swizzling 进行方法交换是一个危险的操作。Stack Overflow 上边有人提出了使用 Method Swizzling 会造成的一些危险和缺陷。更是把 Method Swizzling 比作是厨房里一把锋利的刀。有些人会害怕刀过于锋利,会伤到自己,从而放弃了刀,或者使用了钝刀。但是事实却是:锋利的刀比钝刀反而更加安全,前提是你有足够的经验。
1、应该只在 +load
中执行 Method Swizzling
。
程序在启动的时候,会先加载所有的类,这时会调用每个类的 +load
方法。而且在整个程序运行周期只会调用一次(不包括外部显示调用)。所以在 +load
方法进行 Method Swizzling
再好不过了。
而为什么不用 +initialize
方法呢。
因为 +initialize
方法的调用时机是在 第一次向该类发送第一个消息的时候才会被调用。如果该类只是引用,没有调用,则不会执行 +initialize
方法。
Method Swizzling
影响的是全局状态,+load
方法能保证在加载类的时候就进行交换,保证交换结果。而使用 +initialize
方法则不能保证这一点,有可能在使用的时候起不到交换方法的作用。
2. Method Swizzling
在 +load
中执行时,不要调用 [super load];
。
程序在启动的时候,会先加载所有的类。如果在 + (void)load
方法中调用 [super load]
方法,就会导致父类的 Method Swizzling
被重复执行两次,而方法交换也被执行了两次,相当于互换了一次方法之后,第二次又换回去了,从而使得父类的 Method Swizzling
失效。
3. Method Swizzling
应该总是在 dispatch_once
中执行。
Method Swizzling
不是原子操作,dispatch_once
可以保证即使在不同的线程中也能确保代码只执行一次。所以,我们应该总是在 dispatch_once
中执行 Method Swizzling
操作,保证方法替换只被执行一次。
4. 使用 Method Swizzling
后要记得调用原生方法的实现。
在交换方法实现后记得要调用原生方法的实现(除非你非常确定可以不用调用原生方法的实现):APIs 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现并且一些回调方法不会调用原生方法的实现这可能会造成底层实现的崩溃。
5. 避免命名冲突和参数 _cmd 被篡改。
1、避免命名冲突一个比较好的做法是为替换的方法加个前缀以区别原生方法。一定要确保调用了原生方法的所有地方不会因为自己交换了方法的实现而出现意料不到的结果。
在使用 Method Swizzling 交换方法后记得要在交换方法中调用原生方法的实现。在交换了方法后并且不调用原生方法的实现可能会造成底层实现的崩溃。
2、避免方法命名冲突另一个更好的做法是使用函数指针,也就是上边提到的 方案 B,这种方案能有效避免方法命名冲突和参数 _cmd 被篡改。
6. 谨慎对待 Method Swizzling。
使用 Method Swizzling,会改变非自己拥有的代码。我们使用 Method Swizzling 通常会更改一些系统框架的对象方法,或是类方法。我们改变的不只是一个对象实例,而是改变了项目中所有的该类的对象实例,以及所有子类的对象实例。所以,在使用 Method Swizzling 的时候,应该保持足够的谨慎。
例如,你在一个类中重写一个方法,并且不调用 super 方法,则可能会出现问题。在大多数情况下,super 方法是期望被调用的(除非有特殊说明)。如果你是用同样的思想来进行 Method Swizzling ,可能就会引起很多问题。如果你不调用原始的方法实现,那么你 Method Swizzling 改变的越多代码就越不安全。
7. 对于 Method Swizzling 来说,调用顺序 很重要。
+ load
方法的调用规则为:
1、先调用主类,按照编译顺序,顺序地根据继承关系由父类向子类调用;
2、再调用分类,按照编译顺序,依次调用;
3、+ load 方法除非主动调用,否则只会调用一次。
这样的调用规则导致了 + load
方法调用顺序并不一定确定。一个顺序可能是:父类 -> 子类 -> 父类类别 -> 子类类别
,也可能是 父类 -> 子类 -> 子类类别 -> 父类类别
。所以 Method Swizzling
的顺序不能保证,那么就不能保证 Method Swizzling
后方法的调用顺序是正确的。
所以被用于 Method Swizzling 的方法必须是当前类自身的方法,如果把继承父类来的 IMP 复制到自身上面可能会存在问题。如果 + load 方法调用顺序为:父类 -> 子类 -> 父类类别 -> 子类类别
,那么造成的影响就是调用子类的替换方法并不能正确调起父类分类的替换方法。
8. 在Swift中load方法和initialize方法已经不允许使用了
在Swift中如何使用Runtime
@objc、@objcMembers和dynamic
- (1)如果在项目中想把Swift写的API暴露给Objective-C调用,需要增加@objc.在Swift4中,编译器不在自动为继承自NSObject的派生类添加@objc,需要手动添加
- (2)Swift4中新增了@objcMembers,如果在类前面添加@objcMembers,那么它和它的子类、扩展里的方法都会隐式加上@objc
@objcMembers
class MyClass: NSObject {func print() { } //包含隐式的 @objcfunc show() { } //包含隐式的 @objc
}extension MyClass {func baz() { } //包含隐式的 @objc
}
- (3)dynamic
添加@objc或者@objcMembers并不意味着这个方法或者属性会变成动态派发,Swift依然可能会将其优化为静态调用。如果你需要和Objective-C里动态调用时相同的运行时特性的话,你需要使用dynamic进行修饰,在Swift4中添加dynamic后,必须要手动添加@objc
在桥接文件中导入runtime头文件
#import <objc/runtime.h>
#import <objc/message.h>
Swift中实现Method Swizzling遇到的问题
大神JORDAN SMITH提到,在Swift中使用Method Swizzling遇到的最困难的两个地方:
- (1)swizzling应该只在dispatch_once中完成
- (2)swizzling应该只在+load中完成
在Swift中,load方法和initialize方法都不允许使用了,dispatch_once也被废弃掉了。
关于这两个问题的替代方案: - (1)在Swift中我们使用static let 来替代dispatch_once,let本身就是线程安全的,并且只会被创建一次。
不想定义变量,只想保证一段代码被执行一次,可以使用static let 定义一个Void类型的闭包,看上去是在初始化中立刻执行的闭包,但实际上是懒加载的。
static let doOneTime:Void ={print("just do once");
}()
- (2)在Swift4中+load和+initialize也被废弃了,网上有一种解决方案:UIApplication的next属性会在applicationDidFinishLaunching之前被调用,所以在next属性中通过runtime获取所有类的列表,然后向所有遵循SelfAware协议的类发送消息awake,所有遵循SelfAware协议的类都实现awake方法,在里面进行method Swizzling,这样就保证了method Swizzling被执行的合适时机。
Swift中实战Method Swizzling
方法一
protocol SelfAware:class {static func awake()
}class NothingToSeeHere {//通过runtime获取所有类的列表,然后向所有遵循SelfAware协议的类发送消息static func harmlessFunction() {let typeCount = Int(objc_getClassList(nil, 0))let types = UnsafeMutablePointer<AnyClass>.allocate(capacity: typeCount)let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass>(types)objc_getClassList(autoreleasingTypes, Int32(typeCount))for index in 0 ..< typeCount {(types[index] as? SelfAware.Type)?.awake()}types.deallocate()}
}extension UIApplication {// 只会执行一次,相当于dispatch_onceprivate static let runOnce:Void = {NothingToSeeHere.harmlessFunction()}()// UIApplication的next属性,next属性会在applicationDidFinishLaunching之前被调用。open override var next: UIResponder?{UIApplication.runOncereturn super.next}
}extension UIViewController : SelfAware {static func awake() {UIViewController.classInit()}static func classInit() {swizzledMethod}@objc func swizzled_viewWillAppear(_ animated:Bool){swizzled_viewWillAppear(animated)print("swizzled_viewWillAppear")}private static let swizzledMethod:Void = {let originalSelector = #selector(UIViewController.viewWillAppear(_:))let swizzledSelector = #selector(UIViewController.swizzled_viewWillAppear(_:))swizzlingForClass(HomeController.self, originalSelector, swizzledSelector)}()private static func swizzlingForClass(_ forClass:AnyClass,_ originalSelector:Selector,_ swizzlingSelector:Selector) {// 获取原来方法的地址let originalMethod = class_getInstanceMethod(forClass, originalSelector)// 获取swizzling方法地址let swizzlingMethod = class_getInstanceMethod(forClass, swizzlingSelector)guard let aMethod = originalMethod,let sMethod=swizzlingMethod else {return}// 重新给方法的地址添加新的实现,如果添加失败,则直接调用交换两个方法的实现if class_addMethod(forClass, originalSelector, method_getImplementation(sMethod), method_getTypeEncoding(sMethod)) && class_addMethod(forClass, swizzlingSelector, method_getImplementation(aMethod), method_getTypeEncoding(aMethod)){//添加成功}else{//直接交换两个方法的实现method_exchangeImplementations(aMethod, sMethod)}}
}
在ViewController加载显示出来后的log是这样的
swizzled_viewWillAppear
viewWillAppear
这里需要说一下的是awake()这个方法在上面这个例子里会被调用很多次,因为其实系统有很多继承自UIViewController的子类,但由于我们的swizzleMethod是一个线程安全的静态变量,所以该swizzling方法仅会被调用一次。
另外还发现一个问题,尽管有了extension UIViewController: SelfAware,在harmlessFunction()
调用的时候,发现UIViewController并没有符合SelfAware,但是UIViewController的所有子类却都符合SelfAware。
方法二
import Foundationextension DispatchQueue {private static var _onceTracker = [String]()public class func once(token: String, block: () -> ()) {objc_sync_enter(self)defer {objc_sync_exit(self)}if _onceTracker.contains(token) {return}_onceTracker.append(token)block()}func async(block: @escaping ()->()) {self.async(execute: block)}func after(time: DispatchTime, block: @escaping ()->()) {self.asyncAfter(deadline: time, execute: block)}
}
import UIKitprivate let onceToken = "Method Swizzling viewWillAppear"
extension UIViewController {public class func initializeMethod() {// Make sure This isn't a subclass of UIViewController, So that It applies to all UIViewController childsif self != UIViewController.self {return}//DispatchQueue函数保证代码只被执行一次,防止又被交换回去导致得不到想要的效果DispatchQueue.once(token: onceToken) {let originalSelector = #selector(UIViewController.viewWillAppear(_:))let swizzledSelector = #selector(UIViewController.swizzled_viewWillAppear(animated:))let originalMethod = class_getInstanceMethod(self, originalSelector)let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)//在进行 Swizzling 的时候,需要用 class_addMethod 先进行判断一下原有类中是否有要替换方法的实现let didAddMethod: Bool = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))//如果 class_addMethod 返回 yes,说明当前类中没有要替换方法的实现,所以需要在父类中查找,这时候就用到 method_getImplemetation 去获取 class_getInstanceMethod 里面的方法实现,然后再进行 class_replaceMethod 来实现 Swizzingif didAddMethod {class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))} else {method_exchangeImplementations(originalMethod!, swizzledMethod!)}}}@objc func swizzled_viewWillAppear(animated: Bool) {//需要注入的代码写在此处view.backgroundColor = UIColor.redself.swizzled_viewWillAppear(animated: animated)}
}
import UIKitprivate let onceToken1 = "UITextField.swizzling_draw"
extension UITextField {@objc func swizzling_draw(_ rect: CGRect) {setValue(UIFont.systemFont(ofSize: 5, weight: UIFont.Weight.thin), forKeyPath: "_placeholderLabel.font")swizzling_draw(rect)}public class func drawMethod() {if self != UITextField.self {return}//DispatchQueue函数保证代码只被执行一次,防止又被交换回去导致得不到想要的效果DispatchQueue.once(token: onceToken1) {let originalSelector = #selector(UITextField.draw(_:))let swizzledSelector = #selector(UITextField.swizzling_draw(_:))let originalMethod = class_getInstanceMethod(self, originalSelector)let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)//在进行 Swizzling 的时候,需要用 class_addMethod 先进行判断一下原有类中是否有要替换方法的实现let didAddMethod: Bool = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))//如果 class_addMethod 返回 yes,说明当前类中没有要替换方法的实现,所以需要在父类中查找,这时候就用到 method_getImplemetation 去获取 class_getInstanceMethod 里面的方法实现,然后再进行 class_replaceMethod 来实现 Swizzingif didAddMethod {class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))} else {method_exchangeImplementations(originalMethod!, swizzledMethod!)}}}
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {UITextField.drawMethod()UIViewController.initializeMethod()return true}
Method Swizzling 应用场景
Method Swizzling 可以交换两个方法的实现,在开发中更多的是应用于系统类库,以及第三方框架的方法替换。在官方不公开源码的情况下,我们可以借助 Runtime 的 Method Swizzling 为原有方法添加额外的功能,这使得我们可以做很多有趣的事情。
当系统自带的方法满足不了你的个性化需求时,这时候需要在保证系统方法原有的基础上,给其拓展一些功能。
实现这一需求有两种方案:
方案一: 自定义一个子类,重写父类的方法,把需要的功能写在里面
方案二: 用runtime交换系统的方法,在调用系统方法的时候其实是调用自己的方法,然后在自己定义的方法里面调用系统的那个方法。
全局页面统计功能
需求:在所有页面添加统计功能,用户每进入一次页面就统计一次。
如果有一天公司产品需要我们来实现这个需求。我们应该如何来实现?
先来思考一下有几种实现方式:
-
第一种:手动添加
直接在所有页面添加一次统计代码。你需要做的是写一份统计代码,然后在所有页面的viewWillAppear:
中不停的进行复制、粘贴。 -
第二种:利用继承
创建基类,所有页面都继承自基类。这样的话只需要在基类的viewWillAppear:
中添加一次统计功能。这样修改代码还是很多,如果所有页面不是一开始继承自定义的基类,那么就需要把所有页面的继承关系修改一下,同样会造成很多重复代码,和极大的工作量。 -
第三种:利用分类 + Method Swizzling
我们可以利用 Category 的特性来实现这个功能。如果一个类的分类重写了这个类的方法之后,那么该类的方法将会失效,起作用的将会是分类中重写的方法。
这样的话,我们可以为 UIViewController 建立一个 Category,在分类中重写 viewWillAppear:
,在其中添加统计代码,然后在所有的控制器中引入这个 Category。但是这样的话,所有继承自 UIViewController 类自身的 viewWillAppear:
就失效了,不会被调用。
这就需要用 Method Swizzling 来实现了。步骤如下:
1、在分类中实现一个自定义的xxx_viewWillAppear:
方法;
2、利用 Method Swizzling
将 viewWillAppear:
和自定义的 xxx_viewWillAppear:
进行方法交换。
3、然后在 xxx_viewWillAppear:
中添加统计代码和调用xxx_viewWillAppear:
实现;
因为两个方法发生了交换,所以最后实质是调用了 viewWillAppear:
方法。
#import "UIViewController+Swizzling.h"
#import <objc/runtime.h>@implementation UIViewController (Swizzling)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class class = [self class];SEL originalSelector = @selector(viewWillAppear:);SEL swizzledSelector = @selector(xxx_viewWillAppear:);Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if (didAddMethod) {class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));} else {method_exchangeImplementations(originalMethod, swizzledMethod);}});
}#pragma mark - Method Swizzling- (void)xxx_viewWillAppear:(BOOL)animated {if (![self isKindOfClass:[UIViewController class]]) { // 剔除系统 UIViewController// 添加统计代码NSLog(@"进入页面:%@", [self class]);}[self xxx_viewWillAppear:animated];
}@end
字体根据屏幕尺寸适配
需求:所有的控件字体必须依据屏幕的尺寸等比缩放。
照例,我们先来想想几种实现方式。
-
第一种:手动修改
所有用到的 UIFont 的地方,手动判断,添加适配代码。一想到那个工作量,不忍直视。 -
第二种:利用宏定义
在 PCH 文件定义一个计算缩放字体的方法。在使用设置字体时,先调用宏定义的缩放字体的方法。但是这样同样需要修改所有用到的 UIFont 的地方。工作量依旧很大。
//宏定义
#define UISCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width)/*** 计算缩放字体的方法*/
static inline CGFloat FontSize(CGFloat fontSize){return fontSize * UISCREEN_WIDTH / XXX_UISCREEN_WIDTH;
}
第三种:利用分类 + Method Swizzling
- 1、为UIFont建立一个Category。
- 2、在分类中实现一个自定义的
xxx_systemFontOfSize
:方法,在其中添加缩放字体的方法。 - 3、利用 Method Swizzling 将
systemFontOfSize:
方法和xxx_systemFontOfSize:
进行方法交换。
#import "UIFont+AdjustSwizzling.h"
#import <objc/runtime.h>#define XXX_UISCREEN_WIDTH 375@implementation UIFont (AdjustSwizzling)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class class = [self class];SEL originalSelector = @selector(systemFontOfSize:);SEL swizzledSelector = @selector(xxx_systemFontOfSize:);Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if (didAddMethod) {class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));} else {method_exchangeImplementations(originalMethod, swizzledMethod);}});
}+ (UIFont *)xxx_systemFontOfSize:(CGFloat)fontSize {UIFont *newFont = nil;newFont = [UIFont xxx_systemFontOfSize:fontSize * [UIScreen mainScreen].bounds.size.width / XXX_UISCREEN_WIDTH];return newFont;
}@end
注意:这种方式只适用于纯代码的情况,关于 XIB 字体根据屏幕尺寸适配,可以参考这篇博文:小生不怕:iOS xib文件根据屏幕等比例缩放的适配
处理按钮重复点击
需求:避免一个按钮被快速多次点击。
- 第一种:利用
Delay
延迟,和不可点击方法。
这种方法很直观,也很简单。但就是工作量很大,需要在所有有按钮的地方添加代码。很不想承认:在之前项目中,我使用的就是这种方式。
- (void)viewDidLoad {[super viewDidLoad];UIButton *button = [[UIButton alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];button.backgroundColor = [UIColor redColor];[button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];[self.view addSubview:button];
}- (void)buttonClick:(UIButton *)sender {sender.enabled = NO;[self performSelector:@selector(changeButtonStatus:) withObject:sender afterDelay:0.8f];NSLog(@"点击了按钮");
}- (void)changeButtonStatus:(UIButton *)sender {sender.enabled = YES;
}
- 第二种:
利用分类 + Method Swizzling
1、为 UIControl 或 UIButton 建立一个 Category。
2、在分类中添加一个 NSTimeIntervalxxx_acceptEventInterval
; 的属性,设定重复点击间隔
3、在分类中实现一个自定义的xxx_sendAction:to:forEvent:
方法,在其中添加限定时间相应的方法。
4、利用 Method Swizzling 将sendAction:to:forEvent:
方法和xxx_sendAction:to:forEvent:
进行方法交换。
#import "UIButton+DelaySwizzling.h"
#import <objc/runtime.h>@interface UIButton()// 重复点击间隔
@property (nonatomic, assign) NSTimeInterval xxx_acceptEventInterval;@end@implementation UIButton (DelaySwizzling)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class class = [self class];SEL originalSelector = @selector(sendAction:to:forEvent:);SEL swizzledSelector = @selector(xxx_sendAction:to:forEvent:);Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if (didAddMethod) {class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));} else {method_exchangeImplementations(originalMethod, swizzledMethod);}});
}- (void)xxx_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {// 如果想要设置统一的间隔时间,可以在此处加上以下几句if (self.xxx_acceptEventInterval <= 0) {// 如果没有自定义时间间隔,则默认为 0.4 秒self.xxx_acceptEventInterval = 0.4;}// 是否小于设定的时间间隔BOOL needSendAction = (NSDate.date.timeIntervalSince1970 - self.xxx_acceptEventTime >= self.xxx_acceptEventInterval);// 更新上一次点击时间戳if (self.xxx_acceptEventInterval > 0) {self.xxx_acceptEventTime = NSDate.date.timeIntervalSince1970;}// 两次点击的时间间隔小于设定的时间间隔时,才执行响应事件if (needSendAction) {[self xxx_sendAction:action to:target forEvent:event];}
}- (NSTimeInterval )xxx_acceptEventInterval{return [objc_getAssociatedObject(self, "UIControl_acceptEventInterval") doubleValue];
}- (void)setXxx_acceptEventInterval:(NSTimeInterval)xxx_acceptEventInterval{objc_setAssociatedObject(self, "UIControl_acceptEventInterval", @(xxx_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}- (NSTimeInterval )xxx_acceptEventTime{return [objc_getAssociatedObject(self, "UIControl_acceptEventTime") doubleValue];
}- (void)setXxx_acceptEventTime:(NSTimeInterval)xxx_acceptEventTime{objc_setAssociatedObject(self, "UIControl_acceptEventTime", @(xxx_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}@end
TableView、CollectionView 异常加载占位图
在项目中遇到网络异常,或者其他各种原因造成 TableView、CollectionView 数据为空的时候,通常需要加载占位图显示。那么加载占位图有没有什么好的方法或技巧?
-
第一种:刷新数据后进行判断
这应该是通常的做法。当返回数据,刷新 TableView、CollectionView 时候,进行判断,如果数据为空,则加载占位图。如果数据不为空,则移除占位图,显示数据。 -
第二种:利用
分类 + Method Swizzling 重写 reloadData 方法
。
以 TableView 为例:
1、为 TableView 建立一个 Category,Category 中添加刷新回调 block 属性、占位图 View 属性。
2、在分类中实现一个自定义的 xxx_reloadData
方法,在其中添加判断是否为空,以及加载占位图、隐藏占位图的相关代码。
3、利用 Method Swizzling
将 reloadData
方法和 xxx_reloadData
进行方法交换。
#import <UIKit/UIKit.h>NS_ASSUME_NONNULL_BEGIN@interface UITableView (ReloadDataSwizzling)@property (nonatomic, assign) BOOL firstReload;
@property (nonatomic, strong) UIView *placeholderView;
@property (nonatomic, copy) void(^reloadBlock)(void);@end/*--------------------------------------*/#import "UITableView+ReloadDataSwizzling.h"
#import "XXXPlaceholderView.h"
#import <objc/runtime.h>@implementation UITableView (ReloadDataSwizzling)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class class = [self class];SEL originalSelector = @selector(reloadData);SEL swizzledSelector = @selector(xxx_reloadData);Method originalMethod = class_getInstanceMethod(class, originalSelector);Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if (didAddMethod) {class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));} else {method_exchangeImplementations(originalMethod, swizzledMethod);}});
}- (void)xxx_reloadData {if (!self.firstReload) {[self checkEmpty];}self.firstReload = NO;[self xxx_reloadData];
}- (void)checkEmpty {BOOL isEmpty = YES; // 判空 flag 标示id <UITableViewDataSource> dataSource = self.dataSource;NSInteger sections = 1; // 默认TableView 只有一组if ([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {sections = [dataSource numberOfSectionsInTableView:self] - 1; // 获取当前TableView 组数}for (NSInteger i = 0; i <= sections; i++) {NSInteger rows = [dataSource tableView:self numberOfRowsInSection:i]; // 获取当前TableView各组行数if (rows) {isEmpty = NO; // 若行数存在,不为空}}if (isEmpty) { // 若为空,加载占位图if (!self.placeholderView) { // 若未自定义,加载默认占位图[self makeDefaultPlaceholderView];}self.placeholderView.hidden = NO;[self addSubview:self.placeholderView];} else { // 不为空,隐藏占位图self.placeholderView.hidden = YES;}
}- (void)makeDefaultPlaceholderView {self.bounds = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);XXXPlaceholderView *placeholderView = [[XXXPlaceholderView alloc] initWithFrame:self.bounds];__weak typeof(self) weakSelf = self;[placeholderView setReloadClickBlock:^{if (weakSelf.reloadBlock) {weakSelf.reloadBlock();}}];self.placeholderView = placeholderView;
}- (BOOL)firstReload {return [objc_getAssociatedObject(self, @selector(firstReload)) boolValue];
}- (void)setFirstReload:(BOOL)firstReload {objc_setAssociatedObject(self, @selector(firstReload), @(firstReload), OBJC_ASSOCIATION_ASSIGN);
}- (UIView *)placeholderView {return objc_getAssociatedObject(self, @selector(placeholderView));
}- (void)setPlaceholderView:(UIView *)placeholderView {objc_setAssociatedObject(self, @selector(placeholderView), placeholderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}- (void (^)(void))reloadBlock {return objc_getAssociatedObject(self, @selector(reloadBlock));
}- (void)setReloadBlock:(void (^)(void))reloadBlock {objc_setAssociatedObject(self, @selector(reloadBlock), reloadBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}@end
APM(应用性能管理)、防止程序崩溃
1、通过 Method Swizzling 替换 NSURLConnection , NSURLSession 相关的原始实现(例如 NSURLConnection 的构造方法和 start 方法),在实现中加入网络性能埋点行为,然后调用原始实现。从而来监控网络。
2、防止程序崩溃,可以通过 Method Swizzling 拦截容易造成崩溃的系统方法,然后在替换方法捕获异常类型 NSException ,再对异常进行处理。最常见的例子就是拦截 arrayWithObjects:count: 方法避免数组越界,这种例子网上很多,就不再展示代码了。
一些利用 Method Swizzling 特性进行 APM(应用性能管理) 的例子:
New Relic:https://newrelic.com
听云 APM:https://www.tingyun.com
NetEaseAPM:http://apm.netease.com/
ONE APM:https://www.oneapm.com/
防止程序崩溃的开源项目:
GitHub:chenfanfang / AvoidCrash
GitHub:ValiantCat / XXShield
拦截点击事件
简介
最近在做项目的整合,原来的项目时要求先登录后使用App,现在修改了逻辑,要求先使用App,在某些地方弹出登录提示框,用户登录成功之后程序执行登录之前的操作。比如当用户点击下单的时候,登录成功之后直接进入支付订单页面。
在这里就是使用了Runtime的Swizzle Method
来拦截点击事件,通过Runtime的动态关联来给每个UIView增加一个isNeedCheckLogin
属性,在拦截的点击事件中判断这个属性是否为true,如果是则停止执行点击事件的内容,发送一个需要登录的通知,将target和action一起发送出去,登录功能接收到通知后进行登录,登录成功之后通过performSelector
发送target和action,当然,使用objc_msgSend
也可以,但是在Swift中objc_msgSend
被禁止了,应为Swift中的函数派发方式有很多种,不在仅仅是消息机制了。
给所有的View事件添加isNeedCheckLogin属性
extension UIView {private struct associatedKeys {static var isNeedCheckLogin = "isNeedCheckLogin"}var isNeedCheckLogin:Bool {set {objc_setAssociatedObject(self, &associatedKeys.isNeedCheckLogin, newValue, .OBJC_ASSOCIATION_RETAIN)}get {return (objc_getAssociatedObject(self, &associatedKeys.isNeedCheckLogin) as? Bool) ?? false}}
}
拦截UIControl控件的点击事件
关于UIControl的事件可以通过hook它的sendAction(_:to:for:)来完成
//拦截UIControl的点击事件
extension UIControl : SelfAware {static func awake() {UIControl.classInit()}static func classInit() {swizzledMethod}private static let swizzledMethod:Void = {let originalSelector = #selector(UIControl.sendAction(_:to:for:))let swizzledSelector = #selector(UIControl.swizzled_sendAction(_:to:for:))swizzlingForClass(UIControl.self, originalSelector, swizzledSelector)}()@objc dynamic func swizzled_sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {if isNeedCheckLogin {if HttpClient.userId == NotLogin {NotificationCenter.default.post(name: NSNotification.Name.GoToLoginNotification, object: target, userInfo: ["selector":action,"sender":self])return}else{swizzled_sendAction(action, to: target, for: event)}}else{swizzled_sendAction(action, to: target, for: event)}}private static func swizzlingForClass(_ forClass:AnyClass,_ originalSelector:Selector,_ swizzlingSelector:Selector) {//获取原来方法的地址let originalMethod = class_getInstanceMethod(forClass, originalSelector)//获取swizzling方法地址let swizzlingMethod = class_getInstanceMethod(forClass, swizzlingSelector)guard let aMethod = originalMethod,let sMethod=swizzlingMethod else {return}//重新给方法的地址添加新的实现,如果添加失败,则直接调用交换两个方法的实现if class_addMethod(forClass, originalSelector, method_getImplementation(sMethod), method_getTypeEncoding(sMethod)) && class_addMethod(forClass, swizzlingSelector, method_getImplementation(aMethod), method_getTypeEncoding(aMethod)){//添加成功}else{//直接交换两个方法的实现method_exchangeImplementations(aMethod, sMethod)}}
}
拦截UITableView的点击事件
关于UITableView的某一行的点击事件,可以先hook它们的setDelegate:方法,从而拿到它们的delegate对象,然后再去hook delegate的didSelectRowAtIndexPath:方法即可。
//Hook UITableView的点击事件
extension UITableView:SelfAware{static func awake() {UITableView.classInit()}static func classInit() {swizzledMethod}private static let swizzledMethod:Void = {let originalSelector = #selector(setter:UITableView.delegate)let swizzledSelector = #selector(UITableView.swizzled_setDelegate(delegate:))swizzlingForClass(UITableView.self, originalSelector, swizzledSelector)}()@objc func swizzled_setDelegate(delegate:UITableViewDelegate?) {swizzled_setDelegate(delegate: delegate)guard let delegate = delegate else { //如果delegate为nil直接返回return}//原实例方法let originalSelector = #selector(delegate.tableView(_:didSelectRowAt:))//替换的实例方法let swizzledSelector = #selector(UITableView.swizzled_tableView(_:didSelectRowAt:))//如果没有实现代理方法,则直接返回let swizzleMethod = class_getInstanceMethod(UITableView.self, swizzledSelector)if swizzleMethod == nil {return}// 向实现 delegate 的类中添加新的方法let didAddMethod = class_addMethod(type(of: delegate), swizzledSelector,method_getImplementation(swizzleMethod!), method_getTypeEncoding(swizzleMethod!))//如果已经添加过了,就不在做实现交换了,防止多次交换if didAddMethod {let didSelectOriginalMethod = class_getInstanceMethod(type(of: delegate), NSSelectorFromString("swizzled_tableView:didSelectRowAt:"))let didSelectSwizzledMethod = class_getInstanceMethod(type(of: delegate), originalSelector)if didSelectSwizzledMethod == nil {return}method_exchangeImplementations(didSelectOriginalMethod!, didSelectSwizzledMethod!)}}@objc func none_tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){}/*利用Method Sizzling技术Hook了UITableView的didSelectRowAt的代理方法,如果有一个类继承自含有tableView的控制器时,如果我们先点击父类,然后退出来,再去打开子类,在子类点击cell会导致崩溃。同样,先打开子类,在打开父类,也会崩溃。如果我们在子类中重写父类的didSelectRowAt方法,但是不调用父类的didSelectRowAt方法,就不会产生崩溃的问题*/@objc func swizzled_tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {if let cell = tableView.cellForRow(at: indexPath) {if cell.isNeedCheckLogin == true {if HttpClient.userId == NotLogin {let selector = NSSelectorFromString("tableView:didSelectRowAtIndexPath:")NotificationCenter.default.post(name: NSNotification.Name.GoToLoginNotification, object: self, userInfo: ["selector":selector,"reponseView":tableView,"indexPath":indexPath])return}}swizzled_tableView(tableView, didSelectRowAt: indexPath)}}private static func swizzlingForClass(_ originClass:AnyClass,_ swizzlingClass:AnyClass,_ originalSelector:Selector,_ swizzlingSelector:Selector) {//获取原来方法的地址let originalMethod = class_getInstanceMethod(originClass, originalSelector)//获取swizzling方法地址let swizzlingMethod = class_getInstanceMethod(swizzlingClass, swizzlingSelector)guard let aMethod = originalMethod,let sMethod=swizzlingMethod else {return}//重新给方法的地址添加新的实现,如果添加失败,则直接调用交换两个方法的实现if class_addMethod(originClass, originalSelector, method_getImplementation(sMethod), method_getTypeEncoding(sMethod)) && class_addMethod(swizzlingClass, swizzlingSelector, method_getImplementation(aMethod), method_getTypeEncoding(aMethod)){//添加成功}else{//直接交换两个方法的实现method_exchangeImplementations(aMethod, sMethod)}}private static func swizzlingForClass(_ forClass:AnyClass,_ originalSelector:Selector,_ swizzlingSelector:Selector) {//获取原来方法的地址let originalMethod = class_getInstanceMethod(forClass, originalSelector)//获取swizzling方法地址let swizzlingMethod = class_getInstanceMethod(forClass, swizzlingSelector)guard let aMethod = originalMethod,let sMethod=swizzlingMethod else {return}//重新给方法的地址添加新的实现,如果添加失败,则直接调用交换两个方法的实现if class_addMethod(forClass, originalSelector, method_getImplementation(sMethod), method_getTypeEncoding(sMethod)) && class_addMethod(forClass, swizzlingSelector, method_getImplementation(aMethod), method_getTypeEncoding(aMethod)){//添加成功}else{//直接交换两个方法的实现method_exchangeImplementations(aMethod, sMethod)}}
}
拦截UICollectionView的点击事件
拦截UICollectionView的cell的点击事件和拦截UITableView的cell的点击事件的原理是一样的。首先去hook UICollectionView的setDelegate方法,拿到delegate对象,然后再去hook delegate的didSelectItemAtIndexPath:方法
//拦截UICollectionView的点击事件
extension UICollectionView : SelfAware {static func awake() {UICollectionView.classInit()}static func classInit() {swizzledMethod}private static let swizzledMethod:Void = {let originalSelector = #selector(setter:UICollectionView.delegate)let swizzledSelector = #selector(UICollectionView.swizzled_setDelegate(delegate:))swizzlingForClass(UICollectionView.self, originalSelector, swizzledSelector)}()@objc func swizzled_setDelegate(delegate:UICollectionViewDelegate?) {swizzled_setDelegate(delegate: delegate)guard let delegate = delegate else {return}let originalSelector = #selector(delegate.collectionView(_:didSelectItemAt:))let swizzledSelector = #selector(UICollectionView.swizzled_collectionView(_:didSelectItemAt:))let swizzleMethod = class_getInstanceMethod(UICollectionView.self, swizzledSelector)if swizzleMethod == nil {return}let didAddMethod = class_addMethod(type(of: delegate), swizzledSelector,method_getImplementation(swizzleMethod!), method_getTypeEncoding(swizzleMethod!))if didAddMethod {let didSelectOriginalMethod = class_getInstanceMethod(type(of: delegate), NSSelectorFromString("swizzled_collectionView:didSelectItemAt:"))let didSelectSwizzledMethod = class_getInstanceMethod(type(of: delegate), originalSelector)if didSelectSwizzledMethod == nil {return}method_exchangeImplementations(didSelectOriginalMethod!, didSelectSwizzledMethod!)}}@objc func swizzled_collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {if let cell = collectionView.cellForItem(at: indexPath) {if cell.isNeedCheckLogin == true {if HttpClient.userId == NotLogin {let selector = NSSelectorFromString("collectionView:didSelectItemAtIndexPath:")NotificationCenter.default.post(name: NSNotification.Name.GoToLoginNotification, object: self, userInfo: ["selector":selector,"reponseView":collectionView,"indexPath":indexPath])return}}}swizzled_collectionView(collectionView, didSelectItemAt: indexPath)}private static func swizzlingForClass(_ originClass:AnyClass,_ swizzlingClass:AnyClass,_ originalSelector:Selector,_ swizzlingSelector:Selector) {//获取原来方法的地址let originalMethod = class_getInstanceMethod(originClass, originalSelector)//获取swizzling方法地址let swizzlingMethod = class_getInstanceMethod(swizzlingClass, swizzlingSelector)guard let aMethod = originalMethod,let sMethod=swizzlingMethod else {return}//重新给方法的地址添加新的实现,如果添加失败,则直接调用交换两个方法的实现if class_addMethod(originClass, originalSelector, method_getImplementation(sMethod), method_getTypeEncoding(sMethod)) && class_addMethod(swizzlingClass, swizzlingSelector, method_getImplementation(aMethod), method_getTypeEncoding(aMethod)){//添加成功}else{//直接交换两个方法的实现method_exchangeImplementations(aMethod, sMethod)}}private static func swizzlingForClass(_ forClass:AnyClass,_ originalSelector:Selector,_ swizzlingSelector:Selector) {//获取原来方法的地址let originalMethod = class_getInstanceMethod(forClass, originalSelector)//获取swizzling方法地址let swizzlingMethod = class_getInstanceMethod(forClass, swizzlingSelector)guard let aMethod = originalMethod,let sMethod=swizzlingMethod else {return}//重新给方法的地址添加新的实现,如果添加失败,则直接调用交换两个方法的实现if class_addMethod(forClass, originalSelector, method_getImplementation(sMethod), method_getTypeEncoding(sMethod)) && class_addMethod(forClass, swizzlingSelector, method_getImplementation(aMethod), method_getTypeEncoding(aMethod)){//添加成功}else{//直接交换两个方法的实现method_exchangeImplementations(aMethod, sMethod)}}
}
注意:在拦截UIControl的点击事件时,addTarget方法不能放在懒加载的闭包中,否则将获取不到target对象。
拦截手势的点击事件
//TODO: 关于手势的还没有解决
//extension UIGestureRecognizer:SelfAware {
// static func awake() {
// UIControl.classInit()
// }
//
// static func classInit() {
// swizzledMethod
// }
// private static let swizzledMethod:Void = {
// let originalSelector = #selector(UITapGestureRecognizer.init(target:action:))
// let swizzledSelector = #selector(UITapGestureRecognizer.swizzled_init(target:action:))
// swizzlingForClass(UIControl.self, originalSelector, swizzledSelector)
// }()
//
// @objc dynamic func swizzled_init(target: Any?, action: Selector?) {
//
// if let view = self.view {
// if view.isNeedCheckLogin {
// if HttpClient.userId.count == 0 {
// NotificationCenter.default.post(name: NSNotification.Name.GoToLoginNotification, object: nil)
// return
// }
// }
// }
// swizzled_init(target: target, action: action)
// }
//
// private static func swizzlingForClass(_ forClass:AnyClass,_ originalSelector:Selector,_ swizzlingSelector:Selector) {
// //获取原来方法的地址
// let originalMethod = class_getInstanceMethod(forClass, originalSelector)
// //获取swizzling方法地址
// let swizzlingMethod = class_getInstanceMethod(forClass, swizzlingSelector)
//
// guard let aMethod = originalMethod,let sMethod=swizzlingMethod else {
// return
// }
// //重新给方法的地址添加新的实现,如果添加失败,则直接调用交换两个方法的实现
// if class_addMethod(forClass, originalSelector, method_getImplementation(sMethod), method_getTypeEncoding(sMethod)) && class_addMethod(forClass, swizzlingSelector, method_getImplementation(aMethod), method_getTypeEncoding(aMethod)){
// //添加成功
// }else{
// //直接交换两个方法的实现
// method_exchangeImplementations(aMethod, sMethod)
// }
// }
//}
使用过程中遇到的问题
- 1、在拦截UIControl的点击事件时,addTarget方法不能放在懒加载的闭包中,否则将获取不到target对象。
- 2、利用Method Sizzling技术Hook了UITableView的didSelectRowAt的代理方法,如果有一个类继承自含有tableView的控制器时,如果我们先点击父类,然后退出来,再去打开子类,在子类点击cell会导致崩溃。同样,先打开子类,在打开父类,也会崩溃。
如果我们在子类中重写父类的didSelectRowAt方法,但是不调用父类的didSelectRowAt方法,就不会产生崩溃的问题
六、获取类详细属性、方法
简述
在苹果官方为我们提供的类中,只能获取一小部分公开的属性和方法。有些我们恰好需要的属性和方法,可能会被官方隐藏了起来,没有直接提供给我们。
那应该如何才能获取一个类中所有的变量和方法,用来查找是否有对我们有用的变量和方法呢?
幸好 Runtime 中为我们提供了一系列 API 来获取 Class (类)的 成员变量( Ivar )、属性( Property )、方法( Method )、协议( Protocol ) 等。我们可以通过这些方法来遍历一个类中的成员变量列表、属性列表、方法列表、协议列表。从而查找我们需要的变量和方法。
比如说遇到这样一个需求:更改 UITextField 占位文字的颜色和字号。实现代码参考 3.1 修改私有属性 中的例子。
获取类详细属性、方法
注意:头文件中需引入 #import <objc/runtime.h>
。
获取类的成员变量列表
// 打印成员变量列表
- (void)printIvarList {unsigned int count;Ivar *ivarList = class_copyIvarList([self class], &count);for (unsigned int i = 0; i < count; i++) {Ivar myIvar = ivarList[i];const char *ivarName = ivar_getName(myIvar);NSLog(@"ivar(%d) : %@", i, [NSString stringWithUTF8String:ivarName]);}free(ivarList);
}
获取类的属性列表
// 打印属性列表
- (void)printPropertyList {unsigned int count;objc_property_t *propertyList = class_copyPropertyList([self class], &count);for (unsigned int i = 0; i < count; i++) {const char *propertyName = property_getName(propertyList[i]);NSLog(@"propertyName(%d) : %@", i, [NSString stringWithUTF8String:propertyName]);}free(propertyList);
}
获取类的方法列表
// 打印方法列表
- (void)printMethodList {unsigned int count;Method *methodList = class_copyMethodList([self class], &count);for (unsigned int i = 0; i < count; i++) {Method method = methodList[i];NSLog(@"method(%d) : %@", i, NSStringFromSelector(method_getName(method)));}free(methodList);
}
获取类所遵循的协议列表
// 打印协议列表
- (void)printProtocolList {unsigned int count;__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);for (unsigned int i = 0; i < count; i++) {Protocol *myProtocal = protocolList[i];const char *protocolName = protocol_getName(myProtocal);NSLog(@"protocol(%d) : %@", i, [NSString stringWithUTF8String:protocolName]);}free(protocolList);
}
应用场景
修改私有属性
需求:更改 UITextField 占位文字的颜色和字号
-
方法 1:通过 attributedPlaceholder 属性修改
我们知道 UITextField 中有 placeholder 属性和 attributedPlaceholder 属性。通过 placeholder 属性只能更改占位文字,无法修改占位文字的字体和颜色。而通过 attributedPlaceholder 属性我们就可以修改 UITextField 占位文字的颜色和字号了。 -
方法2:重写 UITextField 的 drawPlaceholderInRect: 方法修改
实现步骤:
1、自定义一个 XXTextField 继承自 UITextField;
2、重写自定义 XXTextField 的 drawPlaceholderInRect: 方法;
3、在 drawPlaceholderInRect 方法中设置 placeholder 的属性。
- (void)drawPlaceholderInRect:(CGRect)rect {// 计算占位文字的 SizeNSDictionary *attributes = @{NSForegroundColorAttributeName : [UIColor lightGrayColor],NSFontAttributeName : [UIFont systemFontOfSize:15]};CGSize placeholderSize = [self.placeholder sizeWithAttributes:attributes];[self.placeholder drawInRect:CGRectMake(0, (rect.size.height - placeholderSize.height)/2, rect.size.width, rect.size.height) withAttributes: attributes];
}
- 方法 3:利用 Runtime,找到并修改 UITextfield 的私有属性
实现步骤:
1、通过获取类的属性列表和成员变量列表的方法打印 UITextfield 所有属性和成员变量;
2、找到私有的成员变量_placeholderLabel;
3、利用 KVC 对_placeholderLabel
进行修改。
// 打印 UITextfield 的所有属性和成员变量
- (void)printUITextFieldList {unsigned int count;Ivar *ivarList = class_copyIvarList([UITextField class], &count);for (unsigned int i = 0; i < count; i++) {Ivar myIvar = ivarList[i];const char *ivarName = ivar_getName(myIvar);NSLog(@"ivar(%d) : %@", i, [NSString stringWithUTF8String:ivarName]);}free(ivarList);objc_property_t *propertyList = class_copyPropertyList([UITextField class], &count);for (unsigned int i = 0; i < count; i++) {const char *propertyName = property_getName(propertyList[i]);NSLog(@"propertyName(%d) : %@", i, [NSString stringWithUTF8String:propertyName]);}free(propertyList);
}// 通过修改 UITextfield 的私有属性更改占位颜色和字体
- (void)createLoginTextField {UITextField *loginTextField = [[UITextField alloc] init];loginTextField.frame = CGRectMake(15,(self.view.bounds.size.height-52-50)/2, self.view.bounds.size.width-60-18,52);loginTextField.delegate = self;loginTextField.font = [UIFont systemFontOfSize:14];loginTextField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;loginTextField.textColor = [UIColor blackColor];loginTextField.placeholder = @"用户名/邮箱";[loginTextField setValue:[UIFont systemFontOfSize:15] forKeyPath:@"_placeholderLabel.font"];[loginTextField setValue:[UIColor lightGrayColor]forKeyPath:@"_placeholderLabel.textColor"];[self.view addSubview:loginTextField];
}
万能控制器跳转
需求:
1、某个页面的不同 banner 图,点击可以跳转到不同页面。
2、推送通知,点击跳转到指定页面。
3、二维码扫描,根据不同内容,跳转不同页面。
4、WebView 页面,根据 URL 点击不同,跳转不同的原生页面。
解决办法:
方法 1:在每个需要跳转的地方写一堆判断语句以及跳转语句。
方法 2:将判断语句和跳转语句抽取出来,写到基类,或者对应的 Category 中。
方法 3:利用 Runtime,定制一个万能跳转控制器工具。
实现步骤:
1、事先和服务器端商量好,定义跳转不同控制器的规则,让服务器传回对应规则的相关参数。
比如:跳转到 A 控制器,需要服务器传回 A 控制器的类名,控制器 A 需要传入的属性参数(id、type 等等)。
2、根据服务器传回的类名,创建对应的控制器对象;
3、遍历服务器传回的参数,利用 Runtime 遍历控制器对象的属性列表;
4、如果控制器对象存在该属性,则利用 KVC 进行赋值;
5、进行跳转。
首先,定义跳转规则,如下所示。XXViewController
是将要跳转的控制器类名。property 字典中保存的是控制器所需的属性参数。
// 定义的规则
NSDictionary *params = @{@"class" : @"XXViewController",@"property" : @{@"ID" : @"123",@"type" : @"XXViewController1"}};
然后,添加一个工具类 XXJumpControllerTool
,添加跳转相关的类方法。
/********************* XXJumpControllerTool.h 文件 *********************/#import <Foundation/Foundation.h>@interface XXJumpControllerTool : NSObject+ (void)pushViewControllerWithParams:(NSDictionary *)params;@end/********************* XXJumpControllerTool.m 文件 *********************/#import "XXJumpControllerTool.h"
#import <UIKit/UIKit.h>
#import <objc/runtime.h>@implementation XXJumpControllerTool+ (void)pushViewControllerWithParams:(NSDictionary *)params {// 取出控制器类名NSString *classNameStr = [NSString stringWithFormat:@"%@", params[@"class"]];const char *className = [classNameStr cStringUsingEncoding:NSASCIIStringEncoding];// 根据字符串返回一个类Class newClass = objc_getClass(className);if (!newClass) {// 创建一个类Class superClass = [NSObject class];newClass = objc_allocateClassPair(superClass, className, 0);// 注册你创建的这个类objc_registerClassPair(newClass);}// 创建对象(就是控制器对象)id instance = [[newClass alloc] init];NSDictionary *propertys = params[@"property"];[propertys enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {// 检测这个对象是否存在该属性if ([XXJumpControllerTool checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {// 利用 KVC 对控制器对象的属性赋值[instance setValue:obj forKey:key];}}];// 跳转到对应的控制器[[XXJumpControllerTool topViewController].navigationController pushViewController:instance animated:YES];
}// 检测对象是否存在该属性
+ (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName {unsigned int count, i;// 获取对象里的属性列表objc_property_t *properties = class_copyPropertyList([instance class], &count);for (i = 0; i < count; i++) {objc_property_t property =properties[i];// 属性名转成字符串NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];// 判断该属性是否存在if ([propertyName isEqualToString:verifyPropertyName]) {free(properties);return YES;}}free(properties);return NO;
}// 获取当前显示在屏幕最顶层的 ViewController
+ (UIViewController *)topViewController {UIViewController *resultVC = [XXJumpControllerTool _topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];while (resultVC.presentedViewController) {resultVC = [XXJumpControllerTool _topViewController:resultVC.presentedViewController];}return resultVC;
}+ (UIViewController *)_topViewController:(UIViewController *)vc {if ([vc isKindOfClass:[UINavigationController class]]) {return [XXJumpControllerTool _topViewController:[(UINavigationController *)vc topViewController]];} else if ([vc isKindOfClass:[UITabBarController class]]) {return [XXJumpControllerTool _topViewController:[(UITabBarController *)vc selectedViewController]];} else {return vc;}return nil;
}@end
测试代码:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {// 万能跳转控制器[self jumpController];
}
实现字典转模型
在日常开发中,将网络请求中获取的 JSON 数据转为数据模型,是我们开发中必不可少的操作。通常我们会选用诸如 YYModel、JSONModel 或者 MJExtension 等第三方框架来实现这一过程。这些框架实现原理的核心就是 Runtime 和 KVC,以及 Getter / Setter。
实现的大体思路如下:借助 Runtime 可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为 key,在 JSON 字典中寻找对应的值 value;再使用 KVC 或直接调用 Getter / Setter 将每一个对应 value 赋值给模型,就完成了字典转模型的目的。
需求:将服务器返回的 JSON 字典转为数据模型。
先准备一份待解析的 JSON 数据,内容如下:
{"id": "123412341234","name": "行走少年郎","age": "18","weight": 120,"address": {"country": "中国","province": "北京"},"courses": [{"name": "Chinese","desc": "语文课"},{"name": "Math","desc": "数学课"},{"name": "English","desc": "英语课"}]
}
假设这就是服务器返回的 JSON 数据,内容是一个学生的信息。现在我们需要将该 JSON 字典转为方便开发的数据模型。
从这份 JSON 中可以看出,字典中取值除了字符串之外,还有数组和字典。那么在将字典转换成数据模型的时候,就要考虑 模型嵌套模型、模型嵌套模型数组 的情况了。具体步骤如下:
创建模型
经过分析,我们总共需要三个模型: XXStudentModel、XXAdressModel、XXCourseModel。
/********************* XXStudentModel.h 文件 *********************/
#import <Foundation/Foundation.h>
#import "NSObject+XXModel.h"
@class XXAdressModel, XXCourseModel;@interface XXStudentModel : NSObject <XXModel>/* 姓名 */
@property (nonatomic, copy) NSString *name;
/* 学生号 id */
@property (nonatomic, copy) NSString *uid;
/* 年龄 */
@property (nonatomic, assign) NSInteger age;
/* 体重 */
@property (nonatomic, assign) NSInteger weight;
/* 地址(嵌套模型) */
@property (nonatomic, strong) XXAdressModel *address;
/* 课程(嵌套模型数组) */
@property (nonatomic, strong) NSArray *courses;@end/********************* XXStudentModel.m 文件 *********************/
#import "XXStudentModel.h"
#import "XXCourseModel.h"@implementation XXStudentModel+ (NSDictionary *)modelContainerPropertyGenericClass {//需要特别处理的属性return @{@"courses" : [XXCourseModel class],@"uid" : @"id"};
}@end/********************* XXAdressModel.h 文件 *********************/
#import <Foundation/Foundation.h>@interface XXAdressModel : NSObject/* 国籍 */
@property (nonatomic, copy) NSString *country;
/* 省份 */
@property (nonatomic, copy) NSString *province;
/* 城市 */
@property (nonatomic, copy) NSString *city;@end/********************* XXAdressModel.m 文件 *********************/
#import "XXAdressModel.h"@implementation XXAdressModel@end/********************* XXCourseModel.h 文件 *********************/
#import <Foundation/Foundation.h>@interface XXCourseModel : NSObject/* 课程名 */
@property (nonatomic, copy) NSString *name;
/* 课程介绍 */
@property (nonatomic, copy) NSString *desc;@end/********************* XXCourseModel.m 文件 *********************/
#import "XXCourseModel.h"@implementation XXCourseModel@end
在 NSObject 分类中实现字典转模型
细心的你可能已经发现:上面的 XXStudentModel.h 文件中导入了 #import "NSObject+XXModel.h"
文件,并且遵循了 <XXModel>
协议,并且在 XXStudentModel.m
文件中实现了协议的 + (NSDictionary *)modelContainerPropertyGenericClass
方法。
NSObject+XXModel.h
、NSObject+XXModel.m
就是我们用来解决字典转模型所创建的分类,协议中的 + (NSDictionary *)modelContainerPropertyGenericClass
方法用来告诉分类特殊字段的处理规则,比如 id --> uid。
/********************* NSObject+XXModel.h 文件 *********************/
#import <Foundation/Foundation.h>// XXModel 协议
@protocol XXModel <NSObject>@optional
// 协议方法:返回一个字典,表明特殊字段的处理规则
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;@end;@interface NSObject (XXModel)// 字典转模型方法
+ (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary;@end
/********************* NSObject+XXModel.m 文件 *********************/
#import "NSObject+XXModel.h"
#import <objc/runtime.h>@implementation NSObject (XXModel)+ (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary {// 创建当前模型对象id object = [[self alloc] init];unsigned int count;// 获取当前对象的属性列表objc_property_t *propertyList = class_copyPropertyList([self class], &count);// 遍历 propertyList 中所有属性,以其属性名为 key,在字典中查找 valuefor (unsigned int i = 0; i < count; i++) {// 获取属性objc_property_t property = propertyList[i];const char *propertyName = property_getName(property);NSString *propertyNameStr = [NSString stringWithUTF8String:propertyName];// 获取 JSON 中属性值 valueid value = [dictionary objectForKey:propertyNameStr];// 获取属性所属类名NSString *propertyType;unsigned int attrCount;objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);for (unsigned int i = 0; i < attrCount; i++) {switch (attrs[i].name[0]) {case 'T': { // Type encodingif (attrs[i].value) {propertyType = [NSString stringWithUTF8String:attrs[i].value];// 去除转义字符:@\"NSString\" -> @"NSString"propertyType = [propertyType stringByReplacingOccurrencesOfString:@"\"" withString:@""];// 去除 @ 符号propertyType = [propertyType stringByReplacingOccurrencesOfString:@"@" withString:@""];}} break;default: break;}}// 对特殊属性进行处理// 判断当前类是否实现了协议方法,获取协议方法中规定的特殊属性的处理方式NSDictionary *perpertyTypeDic;if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil];}// 处理:字典的 key 与模型属性不匹配的问题,如 id -> uidid anotherName = perpertyTypeDic[propertyNameStr];if(anotherName && [anotherName isKindOfClass:[NSString class]]){value = dictionary[anotherName];}// 处理:模型嵌套模型的情况if ([value isKindOfClass:[NSDictionary class]] && ![propertyType hasPrefix:@"NS"]) {Class modelClass = NSClassFromString(propertyType);if (modelClass != nil) {// 将被嵌套字典数据也转化成Modelvalue = [modelClass xx_modelWithDictionary:value];}}// 处理:模型嵌套模型数组的情况// 判断当前 value 是一个数组,而且存在协议方法返回了 perpertyTypeDicif ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) {Class itemModelClass = perpertyTypeDic[propertyNameStr];//封装数组:将每一个子数据转化为 ModelNSMutableArray *itemArray = @[].mutableCopy;for (NSDictionary *itemDic in value) {id model = [itemModelClass xx_modelWithDictionary:itemDic];[itemArray addObject:model];}value = itemArray;}// 使用 KVC 方法将 value 更新到 object 中if (value != nil) {[object setValue:value forKey:propertyNameStr];}}free(propertyList);return object;
}@end
测试代码
- (void)parseJSON {NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Student" ofType:@"json"];NSData *jsonData = [NSData dataWithContentsOfFile:filePath];// 读取 JSON 数据NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];NSLog(@"%@",json);// JSON 字典转模型XXStudentModel *student = [XXStudentModel xx_modelWithDictionary:json];NSLog(@"student.uid = %@", student.uid);NSLog(@"student.name = %@", student.name);for (unsigned int i = 0; i < student.courses.count; i++) {XXCourseModel *courseModel = student.courses[i];NSLog(@"courseModel[%d].name = %@ .desc = %@", i, courseModel.name, courseModel.desc);}
}
当然,如若需要考虑缓存机制、性能问题、对象类型检查等,建议还是使用例如 YYModel 之类的知名第三方框架,或者自己造轮子。
改进 iOS 归档和解档
『归档』是一种常用的轻量型文件存储方式,在项目中,如果需要将数据模型本地化存储,一般就会用到归档和解档。但是如果数据模型中有多个属性的话,我们不得不对每个属性进行处理,这个过程非常繁琐。
这里我们可以参考之前『字典转模型』 的代码。通过 Runtime 获取类的属性列表,实现自动归档和解档。归档操作和解档操作主要会用到了两个方法: encodeObject: forKey: 和 decodeObjectForKey:。
首先在 NSObject 的分类 NSObject+XXModel.h、NSObject+XXModel.m 中添加以下代码:
// 解档
- (instancetype)xx_modelInitWithCoder:(NSCoder *)aDecoder {if (!aDecoder) return self;if (!self) {return self;}unsigned int count;objc_property_t *propertyList = class_copyPropertyList([self class], &count);for (unsigned int i = 0; i < count; i++) {const char *propertyName = property_getName(propertyList[i]);NSString *name = [NSString stringWithUTF8String:propertyName];id value = [aDecoder decodeObjectForKey:name];[self setValue:value forKey:name];}free(propertyList);return self;
}// 归档
- (void)xx_modelEncodeWithCoder:(NSCoder *)aCoder {if (!aCoder) return;if (!self) {return;}unsigned int count;objc_property_t *propertyList = class_copyPropertyList([self class], &count);for (unsigned int i = 0; i < count; i++) {const char *propertyName = property_getName(propertyList[i]);NSString *name = [NSString stringWithUTF8String:propertyName];id value = [self valueForKey:name];[aCoder encodeObject:value forKey:name];}free(propertyList);
}
然后在需要实现归档解档的模型中,添加 -initWithCoder: 和 -encodeWithCoder: 方法。
#import "XXPerson.h"
#import "NSObject+XXModel.h"@implementation XXPerson- (instancetype)initWithCoder:(NSCoder *)aDecoder {self = [super init];if (self) {[self xx_modelInitWithCoder:aDecoder];}return self;
}- (void)encodeWithCoder:(NSCoder *)aCoder {[self xx_modelEncodeWithCoder:aCoder];
}@end
测试一下归档解档代码:
XXPerson *person = [[XXPerson alloc] init];
person.uid = @"123412341234";
person.name = @"行走少年郎";
person.age = 18;
person.weight = 120;// 归档
NSString *path = [NSString stringWithFormat:@"%@/person.plist", NSHomeDirectory()];
[NSKeyedArchiver archiveRootObject:person toFile:path];// 解档
XXPerson *personObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path];NSLog(@"personObject.uid = %@", personObject.uid);
NSLog(@"personObject.name = %@", personObject.name);
当然,上边代码只是演示一下 Runtime 对于归档和解档的优化,真正用在开发中的逻辑远比上边的样例要负责,具体也参考 YYModel 的实现。
七、关联对象(Associated Object)
什么是关联对象
关联对象
分类(category)与关联对象(Associated Object)
作为objective-c的扩展机制的两个特性:分类,可以通过它来扩展方法;Associated Object
,可以通过它来扩展属性;
在iOS开发中,可能category比较常见,相对的Associated Object
,就用的比较少,要用它之前,必须导入<objc/runtime.h>
的头文件。
如何关联对象
runtime提供了給我们3个API以管理关联对象(存储、获取、移除):
//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)
其中的参数:
id object:被关联的对象
const void *key:关联的key,要求唯一
id value:关联的对象
objc_AssociationPolicy policy:内存管理的策略
关联对象:为分类添加“属性”
分类的限制
先来看@property 的一个例子
@interface Person : NSObject@property (nonatomic, strong) NSString *name;@end
在使用上述@property 时会做三件事:
生成实例变量 _property
生成 getter 方法 - property
生成 setter 方法 - setProperty:
@implementation DKObject {NSString *_property;
}- (NSString *)property {return _property;
}- (void)setProperty:(NSString *)property {_property = property;
}@end
这些代码都是编译器为我们生成的,虽然你看不到它,但是它确实在这里。但是,如果我们在分类中写一个属性,则会给一个警告,分类中的 @property 并没有为我们生成实例变量以及存取方法,而需要我们手动实现。
因为在分类中 @property 并不会自动生成实例变量以及存取方法,所以一般使用关联对象为已经存在的类添加 “属性”。解决方案:可以使用两个方法 objc_getAssociatedObject 以及 objc_setAssociatedObject 来模拟属性 的存取方法,而使用关联对象模拟实例变量。
用法解析
NSObject+AssociatedObject.m
#import "NSObject+AssociatedObject.h"
#import <objc/runtime.h>@implementation NSObject (AssociatedObject)- (void)setAssociatedObject:(id)associatedObject
{objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}- (id)associatedObject
{return objc_getAssociatedObject(self, _cmd);
}@end
ViewController.m
- (void)viewDidLoad {[super viewDidLoad];NSObject *objc = [[NSObject alloc] init];objc.associatedObject = @"Extend Category";NSLog(@"associatedObject is = %@", objc.associatedObject);
}
其中, _cmd 代指当前方法的选择子,也就是 @selector(categoryProperty)。_cmd在Objective-C的方法中表示当前方法的selector,正如同self表示当前方法调用的对象实例。这里强调当前,_cmd的作用域只在当前方法里,直指当前方法名@selector。
因而,亦可以写成下面的样子:
- (id)associatedObject
{return objc_getAssociatedObject(self, @selector(associatedObject));
}
另外,查看OBJC_ASSOCIATION_RETAIN_NONATOMIC,可以发现它是一个枚举类型,完整枚举项如下所示:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied. * The association is not made atomically. */OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.* The association is made atomically. */OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.* The association is made atomically. */
};
从这里的注释我们能看到很多东西,也就是说不同的 objc_AssociationPolicy 对应了不通的属性修饰符,整理成表格如下:
objc_AssociationPolicy | modifier |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, strong |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
OBJC_ASSOCIATION_RETAIN | atomic, strong |
OBJC_ASSOCIATION_COPY | atomic, copy |
而我们在代码中实现的属性 associatedObject 就相当于使用了 nonatomic 和 strong 修饰符。
实战场景
利用关联属性实现导航栏渐变
extension UINavigationBar {private struct associateKey {static var alphaViewKey = "alphaViewKey"}var alphaView:UIView? {set {objc_setAssociatedObject(UINavigationBar.self, &associateKey.alphaViewKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)}get {return objc_getAssociatedObject(UINavigationBar.self, &associateKey.alphaViewKey) as? UIView}}func setAlphaNavigation(color:UIColor){if self.alphaView == nil {setBackgroundImage(UIImage(), for: .default)shadowImage = UIImage() //去掉导航栏下面的黑线self.alphaView = UIView(frame: CGRect(x: 0, y: -20, width: UIScreen.main.bounds.width, height: 64))insertSubview(alphaView!, at: 0)}self.alphaView?.backgroundColor = color}
}extension HomeController:UIScrollViewDelegate{func scrollViewDidScroll(_ scrollView: UIScrollView) {let color = UIColor.redlet scrollY:CGFloat = scrollView.contentOffset.ylet alpha = scrollY / 64.0if alpha == 0 {navigationController?.navigationBar.setAlphaNavigation(color: color.withAlphaComponent(0))}else if alpha > 1{navigationController?.navigationBar.setAlphaNavigation(color: color.withAlphaComponent(1))}else {navigationController?.navigationBar.setAlphaNavigation(color: color.withAlphaComponent(alpha))}}
}
UIImageView
需求:比如你为UIView添加事件,可以在上面添加一个UITapGestureRecognizer,但是这个点击事件无法携带NSString信息(虽然可以携带int类型的tag),这就无法让后续响应该事件的方法区分到底是哪里激活的事件。那么,你是否能为这种添加事件的方式携带另外的信息呢?
方案就是为UITapGestureRecognizer追加一个“属性”,利用runtime新建一个UITapGestureRecognizer的分类即可。
分类:
UITapGestureRecognizer+NSString.h
#import <UIKit/UIKit.h>@interface UITapGestureRecognizer (NSString)
//类拓展添加属性
@property (nonatomic, strong) NSString *dataStr;@end
UITapGestureRecognizer+NSString.m
#import "UITapGestureRecognizer+NSString.h"
#import <objc/runtime.h>
//定义常量 必须是C语言字符串
static char *PersonNameKey = "PersonNameKey";@implementation UITapGestureRecognizer (NSString)- (void)setDataStr:(NSString *)dataStr{objc_setAssociatedObject(self, PersonNameKey, dataStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
}-(NSString *)dataStr{return objc_getAssociatedObject(self, PersonNameKey);
}@end
调用处:
VC的tableView:cellForRowAtIndexPath:代理方法中由cell激发事件
UITapGestureRecognizer *signViewSingle0 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];//partnercode
signViewSingle0.dataStr = [cell.cellMdl.partnercode copy];
[cell.contractView addGestureRecognizer:signViewSingle0];
VC单独写一个响应方法
- (void)tapAction:(UITapGestureRecognizer *)sender
{UITapGestureRecognizer *tap = (UITapGestureRecognizer *)sender;//partnercode[self requestCallConSetWithPartnerCode:tap.dataStr];
}
如此一来,响应事件的方法就可以根据事件激活方携带过来的信息进行下一步操作了,比如根据它携带过来的某个参数进行网络请求等等。
为UI控件关联事件Block体
UIAlertView
开发iOS时经常用到UIAlertView类,该类提供了一种标准视图,可向用户展示警告信息。当用户按下按钮关闭该视图时,需要用委托协议(delegate protocol)来处理此动作,但是,要想设置好这个委托机制,就得把创建警告视图和处理按钮动作的代码分开。由于代码分作两块,所以读起来有点乱。
- 方案1 :传统方案
比方说,我们在使用UIAlertView时,一般都会这么写:
Test2ViewController
- (void)viewDidLoad {[super viewDidLoad];[self.view setBackgroundColor:[UIColor whiteColor]];self.title = @"Test2ViewController";[self popAlertViews1];
}#pragma mark - way1
- (void)popAlertViews1{UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];[alert show];
}// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{if (buttonIndex == 0) {[self doCancel];} else {[self doContinue];}
}
如果想在同一个类里处理多个警告信息视图,那么代码就会变得更为复杂,我们必须在delegate方法中检查传入的alertView参数,并据此选用相应的逻辑。
要是能在创建UIAlertView的时候直接把处理每个按钮的逻辑都写好,那就简单多了。这可以通过关联对象来做。创建完警告视图之后,设定一个与之关联的“块”(block),等到执行delegate方法时再将其读出来。下面对此方案进行改进。
- 方案2:关联Block体
除了上一个方案中的传统方法,我们可以利用关联对象为UIAlertView关联一个Block:首先在创建UIAlertView的时候设置关联一个回调(objc_setAssociatedObject),然后在UIAlertView的代理方法中取出关联相应回调(objc_getAssociatedObject)。
Test2ViewController.m
#pragma mark - way2
- (void)popAlertViews2 {UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];void (^clickBlock)(NSInteger) = ^(NSInteger buttonIndex){if (buttonIndex == 0) {[self doCancel];} else {[self doContinue];}};objc_setAssociatedObject(alert,CMAlertViewKey,clickBlock,OBJC_ASSOCIATION_COPY);[alert show];
}// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{void (^clickBlock)(NSInteger) = objc_getAssociatedObject(alertView, CMAlertViewKey);clickBlock(buttonIndex);
}
方案3:继续改进:封装关联的Block体,作为属性
上面方案,如果需要的位置比较多,相同的代码会比较冗余地出现,所以我们可以将设置Block的代码封装到一个UIAlertView的分类中去。
UIAlertView+Handle.h
#import <UIKit/UIKit.h>// 声明一个button点击事件的回调block
typedef void (^ClickBlock)(NSInteger buttonIndex) ;@interface UIAlertView (Handle)@property (copy, nonatomic) ClickBlock callBlock;@end
UIAlertView+Handle.m
#import "UIAlertView+Handle.h"
#import <objc/runtime.h>@implementation UIAlertView (Handle)- (void)setCallBlock:(ClickBlock)callBlock
{objc_setAssociatedObject(self, @selector(callBlock), callBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}- (ClickBlock )callBlock
{return objc_getAssociatedObject(self, _cmd);// return objc_getAssociatedObject(self, @selector(callBlock));
}@end
Test2ViewController.m
#pragma mark - way3
- (void)popAlertViews3 {UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];[alert setCallBlock:^(NSInteger buttonIndex) {if (buttonIndex == 0) {[self doCancel];} else {[self doContinue];}}];[alert show];
}// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{void (^block)(NSInteger) = alertView.callBlock;block(buttonIndex);
}
- 方案4:继续改进:封装关联的Block体,跟初始化方法绑在一起
UIButton
除了上述的UIAlertView,这节以UIButton为例,使用关联对象完成一个功能函数:为UIButton增加一个分类,定义一个方法,使用block去实现button的点击回调。
UIButton+Handle.h
#import <UIKit/UIKit.h>
#import <objc/runtime.h> // 导入头文件// 声明一个button点击事件的回调block
typedef void(^ButtonClickCallBack)(UIButton *button);@interface UIButton (Handle)// 为UIButton增加的回调方法
- (void)handleClickCallBack:(ButtonClickCallBack)callBack;@end
UIButton+Handle.m
#import "UIButton+Handle.h"// 声明一个静态的索引key,用于获取被关联对象的值
static char *buttonClickKey;@implementation UIButton (Handle)- (void)handleClickCallBack:(ButtonClickCallBack)callBack {// 将button的实例与回调的block通过索引key进行关联:objc_setAssociatedObject(self, &buttonClickKey, callBack, OBJC_ASSOCIATION_RETAIN_NONATOMIC);// 设置button执行的方法[self addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];
}- (void)buttonClicked {// 通过静态的索引key,获取被关联对象(这里就是回调的block)ButtonClickCallBack callBack = objc_getAssociatedObject(self, &buttonClickKey);if (callBack) {callBack(self);}
}@end
在Test3ViewController中,导入我们写好的UIButton分类头文件,定义一个button对象,调用分类中的这个方法:
- Test3ViewController.m
[self.testButton handleClickCallBack:^(UIButton *button) {NSLog(@"block --- click UIButton+Handle");}];
关联对象:关联观察者对象
有时候我们在分类中使用NSNotificationCenter或者KVO,推荐使用关联的对象作为观察者,尽量避免对象观察自身。
例如大名鼎鼎的AFNetworking为菊花控件监听NSURLSessionTask以获取网络进度的分类:
UIActivityIndicatorView+AFNetworking.m
@implementation UIActivityIndicatorView (AFNetworking)- (AFActivityIndicatorViewNotificationObserver *)af_notificationObserver {AFActivityIndicatorViewNotificationObserver *notificationObserver = objc_getAssociatedObject(self, @selector(af_notificationObserver));if (notificationObserver == nil) {notificationObserver = [[AFActivityIndicatorViewNotificationObserver alloc] initWithActivityIndicatorView:self];objc_setAssociatedObject(self, @selector(af_notificationObserver), notificationObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}return notificationObserver;
}- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {[[self af_notificationObserver] setAnimatingWithStateOfTask:task];
}@end
@implementation AFActivityIndicatorViewNotificationObserver- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task {NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];[notificationCenter removeObserver:self name:AFNetworkingTaskDidResumeNotification object:nil];[notificationCenter removeObserver:self name:AFNetworkingTaskDidSuspendNotification object:nil];[notificationCenter removeObserver:self name:AFNetworkingTaskDidCompleteNotification object:nil];if (task) {if (task.state != NSURLSessionTaskStateCompleted) {UIActivityIndicatorView *activityIndicatorView = self.activityIndicatorView;if (task.state == NSURLSessionTaskStateRunning) {[activityIndicatorView startAnimating];} else {[activityIndicatorView stopAnimating];}[notificationCenter addObserver:self selector:@selector(af_startAnimating) name:AFNetworkingTaskDidResumeNotification object:task];[notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidCompleteNotification object:task];[notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidSuspendNotification object:task];}}
}
关联对象:为了不重复执行
有时候OC中会有些方法是为了获取某个数据,但这个获取的过程只需要执行一次即可,这个获取的算法可能有一定的时间复杂度和空间复杂度。那么每次调用的时候就必须得执行一次吗?有没有办法让方法只执行一次,每次调用方法的时候直接获得那一次的执行结果?有的,方案就是让某个对象的方法获得的数据结果作为“属性”与这个对象进行关联。
有这么一个需求:需要将字典转成模型对象
方案:我们先获取到对象所有的属性名(只执行一次),然后加入到一个数组里面,然后再遍历,利用KVC进行键值赋值。在程序运行的时候,抓取对象的属性,这时候,要利用到运行时的关联对象了,详情见下面的代码。
- 获取对象所有的属性名
+ (NSArray *)propertyList {// 0. 判断是否存在关联对象,如果存在,直接返回/**1> 关联到的对象2> 关联的属性 key提示:在 OC 中,类本质上也是一个对象*/NSArray *pList = objc_getAssociatedObject(self, propertiesKey);if (pList != nil) {return pList;}// 1. 获取`类`的属性/**参数1> 类2> 属性的计数指针*/unsigned int count = 0;// 返回值是所有属性的数组 objc_property_tobjc_property_t *list = class_copyPropertyList([self class], &count);NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:count];// 遍历数组for (unsigned int i = 0; i < count; ++i) {// 获取到属性objc_property_t pty = list[i];// 获取属性的名称const char *cname = property_getName(pty);[arrayM addObject:[NSString stringWithUTF8String:cname]];}NSLog(@"%@", arrayM);// 释放属性数组free(list);// 设置关联对象/**1> 关联的对象2> 关联对象的 key3> 属性数值4> 属性的持有方式 reatin, copy, assign*/objc_setAssociatedObject(self, propertiesKey, arrayM, OBJC_ASSOCIATION_COPY_NONATOMIC);return arrayM.copy;
}
- KVC进行键值赋值
+ (instancetype)objectWithDict:(NSDictionary *)dict {id obj = [[self alloc] init];// [obj setValuesForKeysWithDictionary:dict];NSArray *properties = [self propertyList];// 遍历属性数组for (NSString *key in properties) {// 判断字典中是否包含这个keyif (dict[key] != nil) {// 使用 KVC 设置数值[obj setValue:dict[key] forKeyPath:key];}}return obj;
}
八、数据结构
objc_object
平时使用的id类型最终会被转换为objc_object。这个结构体包含以下几部分
isa指针是一个isa_t是一个共用体
objc_class
平时使用的Class最终会转换为objc_class,它继承自objc_object,所以说Class也是一个对象,称之为类对象,这个结构体包含:
cache_t是一个方法缓存的结构,消息传递的过程中会用到方法缓存的数据结构
class_data_bits_t:一个类定义的变量、属性、方法都在bits中。
isa
isa指针是一个共用体isa_t
共用体在64位机器上是64个0或者1的数字。 isa分为两种类型的指针,一种是指针型isa,另外一种是非指针类型的isa。 指针型isa指针的值代表的是Class的地址,对于非指针型的isa,它的值的部分代表Class的地址,这么做的目的在寻址过程中只需要30-40位就可以找到Class的地址,其他部分可以存储一些其他内容,达到节省内存的目的。
关于对象,isa指针指向类对象。 关于类,isa指针指向元类对象。
cache_t
cache_t是用于快速查找方法列表的执行函数,它是一个可增量扩展的哈希表结构,它是局部性原理的最佳应用。
cache_t可以理解为是一个数组实现的,每一个数组对象都是一个bucket_t结构体,bucket_t有两个主要的成员变量:key和IMP。key是OC中的selector,IMP可以理解为一个无类型的函数指针。我们拿到一个key,可以通过哈希查找算法来定位当前key所在的bucket_t所在的位置,然后可以获取bucket_t,获取IMP来调用函数。
class_data_bits_t
class_data_bits_t是对class_rw_t的封装,class_rw_t代表了类相关的读写信息,对class_ro_t的封装。class_ro_t代表了类相关的只读信息。
protocols、properties和methods都是二维数组,继承自list_array_tt这样一个二维数组。
name:代表类名 ivars:成员变量 properties:属性 protocols:协议 methodList:方法列表
这里ivars、properties、protocols、methodList都是一位数组,在class_rw_t中的方法列表等存储的信息是分类中的信息,在class_ro_t的方法列表等存储的就是原生类的信息,methodList里面的是一个method_t的数据结构。
mehod_t
name:方法名称
types:函数的返回值和参数的组合
imp:是一个无类型的函数指针
Type Encodings
const char* type是不可变的字符指针,组成结构大致是下面这样的
-(void) aMethod,它对应的Type Encoding对应的就是一个v@:,这就是types存储的内容,这三个位置就对应图上的位置v代表返回值,@对应参数1,:对应参数2,V就是void,@就是id对象。 我们所调用的一个方法,达到runtime层面的时候都会转化为objc_messageSend这么一个函数,这个函数调用的第一次参数和第二参数是固定的,第一个参数是消息接收者(@),第二个参数是SEL(😃
整体数据结构
类对象与元类对象
类对象和元类对象有什么区别和联系?
实例对象可以通过isa指针找到它的类对象,类对象存储实例方法列表等信息。类对象可以通过isa指针找到它的元类对象,元类对象存储类方法列表等信息。我们的类对象和元类对象都是objc_class数据结构的,这个数据结构继承自objc_object,所以它们才有isa指针。 对于任何一个元类对象的isa指针都指向根元类对象,包括根元类自身。根元类的super class指向根类对象。 我们调用类方法,要从元类对象中去查找,逐级查找,当在根元类中也无法查找到调用的类方法时,就会到根类对象中找同名的实例方法实现。
如果说我们调用的类方法,没有对应的实现,有同名的实例方法的实现,会不会发生崩溃呢? 不会发生崩溃,会执行同名方法的调用。
经典面试题
#import "Mobile.h"
@interface Phone:Mobile@end@implementation Phone- (id) init {self = [super init];if(self) {NSLog(@"%@",NSStringFromClass([self class]));NSLog(@"%@",NSStringFromClass([super class]));}return self;
}@end
[self class]消息的接收者是当前对象,class是在NSObject中才有实现的。[super class]实际接收者仍然是当前对象,只不过是super是从父类对象中开始查找class的实现,最终都是查找到NSObject当中,因为super是objc_super,里面有一个receiver表示当前对象,所以打印结果都是Phone
void objc_msgSend(void /* id self, SEL op, ... */ )
[self class] ==> objc_msgSend(self,@selector(class))
void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
[super class] ==> objc_msgSendSuper(super,@selector(class))
struct objc_super {__unsafe_unretained id receiver;receiver就是当前对象
}
这篇关于24、OC语言的动态性学习(Runtime)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!