前言:我是一个有着两年开发经验的iOS开发者,经过前段时间的试水,从面试中认识到了自己的不足。所以就把面试中遇到的问题和其中牵扯到的知识进行了整理。每一个知识点牵扯到的东西是很多的,所以会有很多遗漏和错误,欢迎大佬们进行指正。我会从几部分进行文章更新。先从runtime开始:
一.RunTime
面试问题:实现原理;请用runtime解释下字典快速转模型MJExtension;performSelector方法的调用;出现unrecognized selector sent to instance XXXXXX错误的原因;KVO;如何通过category给已有的类添加属性(property)
概述:OC本质上是一种基于C语言的领域特定语言,通过用C语言和汇编实现的runtime,在C语言的基础上实现了面向对象的功能。在runtime中,对象用结构体表示,方法用函数表示。C语言是一门静态语言,其在编译时决定调用哪个函数,而OC则是一门动态语言,其在编译时不能决定最终执行时调用哪个函数(OC中函数调用称为消息传递)。OC的这种动态绑定机制正是通过runtime这样一个中间层实现的。
OC类:是由class类型来表示的,本质上是一个objc_class结构体的指针。objc/runtime.h中的定义如下:
typedef struct object_class *Class
struct object_class{
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list *methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
OC对象:是由id类型表示的,本质上是一个指向objc_object结构体的指针,其中只有一个成员,指向其类的isa指针。当向一个OC对象发送消息时,runtime会根据实例对象的isa指针找到其所属的类。Runtime会在类的方法列表以及父类的方法列表中去寻找与消息对应的selector指向的方法,找到后即运行该方法。objc/objc.h中的定义如下:
typedef struct objc_object *id;
struct objc_object{
Class isa OBJC_ISA_AVAILABILITY;
};
OC元类:meta-class是一个类对象的类,在OC中所有的类本身也是一个对象。秉承万物皆对象的设计思想,通过向该对象发送消息,即可实现对类方法的调用。前提是类的指针必须指向一个包含这些类方法的objc_class结构体。meta-class中存储着一个类的所有类方法。所以,类对象的isa指针指向的就是meta-class。
当向一个对象发送消息时,runtime会在这个对象所属的类的方法列表中查找方法。
当向一个类发送消息时,runtime会在这个类的meta-class的方法列表中查找。
meta-class 也是一个类,也可以向它发送一个消息,那么它的 isa 又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C 的设计者让所有的 meta-class 的 isa 指向基类的 meta-class,以此作为它们的所属类。
OC方法:方法实际上是一个指向objc_method结构体的指针,objc/runtime.h中的定义如下:
typedef struct objc_method *Method
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
结构体中包含成员SEL和IMP,两者将方法的名字与实现进行了绑定。通过SEL可以找到对应的IMP,从而调用方法的具体实现。
SEL:选择器,是一个指向objc_selector结构体的指针
typedef struct objc_selector *SEL;
方法的selector用于表示运行时方法的名字,OC在编译时,会根据方法的名字(不包括参数)生成唯一一个整型标识(Int类型的地址),即SEL。
由于一个类的方法列表中不能存在两个相同的 SEL,所以 Objective-C 不支持重载。但是不同类之间可以存在相同的 SEL,因为不同类的实例对象执行相同的 selector 时,会在各自的方法列表中去根据 selector 去寻找自己对应的 IMP。
通过下面三种方法可以获取 SEL:
- sel_registerName 函数
- Objective-C 编译器提供的 @selector() 方法
- NSSeletorFromString() 方法
IMP:本质上就是一个函数指针,指向方法实现的地址
typedef id (*IMP)(id, SEL,...);
参数说明:
id
:指向 self 的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针);
SEL
:方法选择器;
...
:方法的参数列表。
SEL 与 IMP 的关系类似于哈希表中 key 与 value 的关系。采用这种哈希映射的方式可以加快方法的查找速度。
消息传递(方法调用)机制:
消息直到运行时才绑定到方法实现上
id returnValue = [someObject messageName:parameter]
参数说明:
id
返回值类型
returnValue
返回值
someObject
消息接收对象
messageName
选择器
parameter
参数
messageName:parameter
消息
消息传递机制中的核心函数:
void objc_msgSend(id self,SEL cmd,…)
第一个参数表示接受者,第二个参数代表选择器(SEL是选择器的类型),后续参数就是消息中的那些参数,顺序不变。选择器指的就是方法的名字。编译器把上述调用转为如下消息传递:
id returnValue = objc_msgSend(someObject,@selector(messageName),parameter);
消息传递流程:
- 当消息传递给一个对象时,首先从运行时系统缓存 objc_cache 中进行查找。如果找到,则执行。否则,继续执行下面步骤。
- objc_msgSend通过对象的isa指针获取到类的结构体;
- 在方法分发表method_list查找,是否有与选择器名称相符的selector方法;
- 没有则根据所属类的superClass指针,沿着类的继承体系继续向上查找;
- 有则根据IMP指针跳转到方法的实现代码,传入参数调用这个方法的实现,并将该方法加入缓存objc_cache中;
- 转发IMP的return值
- 如果在继承体系中还是没找到与选择器相符的方法,此时就会执行“消息转发”(message forwarding)操作
类缓存(快速映射表):
我们发现调用一个方法并不像我们想的那么简单,更不像我们写的那么简单,一个方法的执行其实底层需要很多步骤。正因如此,objc_msgSend()会将调用过且匹配到的方法缓存在“快速映射表(fast map)”中,快速映射表就是方法的缓存表。每个类都有这样一个缓存。所以,即便子类实例从父类的方法列表中取过了某个对象方法,那么子类的方法缓存表中也会缓存父类的这个方法,下次调用这个方法,会优先去当前类(对象所属的类)的方法缓存表中查找这个方法,这样的好处是显而易见的,减少了漫长的方法查找过程,使得方法的调用更快。同样,如果父类实例对象调用了同样的方法,也会在父类的方法缓存表中缓存这个方法。同理,如果用一个子类对象调用某个类方法,也会在子类的metaclass里缓存一份。而当用一个父类对象去调用那个类方法的时候,也会在父类的metaclass里缓存一份。SEL为键,IMP为值?
消息转发流程:
1.动态方法解析
对象方法:
+(BOOL)resolveInstanceMethod:(SEL)selector
类方法:
+(BOOL)resolveClassMethod:(SEL)selector
以上方法会让你有机会提供一个函数实现,如果添加了函数并且返回了yes,那运行时系统就会重新启动一次消息发送的过程
相关方法:
添加方法
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _nullable types)
参数说明:
cls
:被添加方法的类
name
:添加的方法的名称的SEL
imp
:方法的实现,该函数必须至少要有两个参数,self,_cmd
types
:代表函数类型,比如无参数无返回值->”v@:”,int类型返回值,一个参数传入->”i@:@”,如果你知道了对应的Method,你可以直接通过method_getTypeEncoding函数获取。
获取实例方法
Method class_getInstanceMethod(Class cls, SEL name);
获取类方法
Method class_getClassMethod(Class cls,SEL name);
获取所有方法的Llist
Method *class_copyMethodList(Class cls, unsigned int *outCount);
替代方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
返回方法的具体实现
IMP class_getMethodImplementation(Class cls, SEL name);
IMP class_getMethodImplementation_stret(Class cls, SEL name);
类实例是否响应指定的selector
BOOL class_respondsToSelector(Class cls,SEL name);
动态解析方法示例:
#import "ViewController.h"
#import "objc/message.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//通过performSelector方法调用对象方法,体现运行时机制
[self performSelector:@selector(test:)];
}
//调用对象方法后,系统在所属类对象以及所在继承体系中的super_class中的结构体中的method_list中查找匹配的方法,未找到会先调用下面的方法,让你有机会可以添加一个方法并且返回yes进行消息传递重启
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"test:")) {//如果是test方法则进行添加
return class_addMethod([self class], sel, (IMP)myTest,"v@:");
}
return [super resolveInstanceMethod:sel];//调用父类的方法
}
void myTest(id obj, SEL _cmd) {//添加的方法实现
NSLog(@"Doing test");
}
2.备用接收者
如果在resolveMethod方法中返回NO并且消息接收者实现了-forwardingTargetForSelector:
,系统就会调用这个方法,让你有机会把这个消息转发给其他对象。
#import "ViewController.h"
#import "objc/message.h"
@interface Person : NSObject
@end
@implementation Person
- (void)test:(NSString *)name {
NSLog(@"name = %@",name);//person的test:方法
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//通过performSelector方法调用对象方法,体现运行时机制
[self performSelector:@selector(test:) withObject:@"test" withObject:nil];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
//返回NO则会进入下一步消息转发
return NO;
}
//这个方法可以更换消息接受者,之前的消息接受者为self,更换为了person,其实就是消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test:)) {
return [[Person alloc] init];//返回Person对象,让Person对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
打印结果:2019-05-09 16:11:01.306228+0800 Mytest[3978:1797869] name = test
3.完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。首先它会发送-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:
返回nil,Runtime则会发出-doesNotRecognizeSelector:
或者unRecognized selector send to instance XXXX
,程序就会挂掉。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:
消息给目标对象。
#import "ViewController.h"
#import "objc/message.h"
@interface Person : NSObject
@end
@implementation Person
- (void)test:(NSString *)name {
NSLog(@"name = %@",name);//person的test:方法
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//通过performSelector方法调用对象方法,体现运行时机制
[self performSelector:@selector(test:) withObject:@"test" withObject:nil];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
//返回NO则会进入下一步备用接受者
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
//返回nil则会进入下一步完整消息转发
return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test:)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];//签名,进入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Person *p = [[Person alloc] init];
if ([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}else {
[self doesNotRecognizeSelector:sel];
}
}
@end
打印结果:2019-05-09 16:35:45.286281+0800 Mytest[4383:1810623] name = test
如果注释掉person类中的test:方法或者签名方法中返回nil则会报错:
报错信息:2019-05-09 16:37:11.123973+0800 Mytest[4413:1811986] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController test:]: unrecognized selector sent to instance 0x7fbedcc18660'
通过签名Runtime生成了一个对象anInvocation
,我们在forwardInvocation方法里面让Person对象去执行了test:函数。
Runtime在开发中的应用
KVO实现
Key-value-observing
,提供了一种当其他对象属性被修改的时候能通知当前对象的机制,很适合实现model和controller类之间的通讯。
先来看下KVO的简单使用:
#import "ViewController.h"
#import "objc/message.h"
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
p.name = @"oldValue";
//p为被观察者,self为观察者,name为被观察者属性
//1.注册观察者(为被观察者指定观察者以及被观察者属性)
//observer观察者 (观察self.view对象的属性的变化)
//KeyPath: 被观察属性的名称
//options: 观察属性的新值,旧值等的一些配置(枚举值)
//context:上下文 可以为kvo的回调方法传值
//注册观察者(可以是多个)
/*
options: 有4个值,分别是:
NSKeyValueObservingOptionOld 把更改之前的值提供给处理方法
NSKeyValueObservingOptionNew 把更改之后的值提供给处理方法
NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。
*/
NSLog(@"添加观察者之前----p->isa:%@,class=%@",object_getClass(p),[p class]);
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"test"];
NSLog(@"添加观察者之后----p->isa:%@,class=%@",object_getClass(p),[p class]);
//3.属性值发生改变,触发回调方法(需要触发该属性的setter方法才会触发回调)
p.name = @"newValue";
//[p setValue:@"newValue" forKey:@"name"];
}
//2.实现回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"keyPath=%@,object=%@,change=%@,context=%@",keyPath,[object class],change,context);
}
//4.移除观察者
- (void)dealloc {
[self removeObserver:self forKeyPath:@"name"];
}
@end
打印结果:2019-05-10 16:34:50.492298+0800 Mytest[15101:2074793]
添加观察者之前----p->isa:Person,class=Person
2019-05-10 16:34:50.492799+0800 Mytest[15101:2074793]
添加观察者之后----p->isa:NSKVONotifying_Person,class=Person
2019-05-10 16:34:50.493138+0800 Mytest[15101:2074793]keyPath=name,object=Person,change={ kind = 1; new = newValue; old = oldValue; },context=test
KVO的实现依赖于OC强大的runtime机制,当观察某对象A
时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keypath
的setter
方法。setter方法随后负责通知观察对象属性的改变状况。Apple
使用了isa-swizzling
来实现KVO,当观察对象A时,KVO
机制动态创建一个新的名为:NSKVONotififying_A
的新类,该类继承自对象A的本类,且KVO
为NSKVONotifying_A
重写观察属性的setter
方法,setter方法会负责在调用原setter方法之前和之后,通知所有观察对象属性值的更改情况。
如代码和打印结果所示,KVO在添加观察者的过程中,被观察者的isa指针
从指向原来的A类
被改为指向系统所创建的子类NSKVONotifying_A
类以此来实现当前类属性值改变的监听;如果我们自己创建一个新的名为NSKVONotifying_A
的类,就会发现系统在运行到注册KVO的代码时程序崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向了这个中间类。
KVO键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:
和didChangeValueForKey:
,在存取数值的前后分别调用2个方法:
- 被观察属性发生改变之前
willChangeValueForKey:
被调用,通知系统该keyPath的属性值即将变更; - 当改变发生后,
didChangeValueForKey:
被调用,通知系统该keyPath的属性值已经变更;
之后会调用KVO的回调方法observeValueForKeyPath:ofObject:change:context:
,且重写观察属性的setter方法是在运行时而不是编译时实现的。
为子类的观察者属性重写调用存取方法的工作原理相当于:
- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用
}
字典和模型的自动转换MJExtension
用runtime提供的函数遍历model
自身所有属性,如果属性在json中有对应的值,则将其赋值。核心方法:在NSObject
的分类中添加方法:
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
//(1)获取类的属性及属性对应的类型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通过property_getName函数获得属性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通过property_getAttributes函数可以获得属性的名字和@encode编码
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即释放properties指向的内存
free(properties);
//(2)根据类型给属性赋值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
其他应用:(具体参考:iOS Runtime详解)
- 关联对象(
Objective-C Associated Objects
)给分类增加属性; - 方法魔法:方法添加,方法替换;
- 消息转发(热更新)解决bug(
JSPatch
) - 实现
NSCoding
的自动归档和自动解档
performSelector的使用
1)基础用法:
- (id)performSelector:(SEL)aSelector;//无参数传递
- (id)performSelector:(SEL)aSelector withObject:(id)object;//传递一个参数
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;//传递两个参数
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;//主线程执行方法,`wait`参数为YES表示需要阻塞线程,直到主线程将我们的代码块执行完毕
均为同步执行,与线程无关,主线程和子线程均可调用成功,等同于直接调用该方法,不同之处在于如果该方法未实现,直接调用编译时会报错,使用此方法,则是在编译的时候有警告,运行时会报错unrecognized selector sent to instance XXXXX 。使用performSelector
和(BOOL)respondsToSelector:(SEL)aselector
是最佳搭档。
2)延迟执行:
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
异步执行。如果在子线程使用该方法,则需要手动开启runLoop(获取runLoop时会进行创建)
代码示例:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"first");
[self performSelector:@selector(secondMethod)];//同步执行
[self performSelector:@selector(thirdMethod) withObject:nil afterDelay:0.5];//异步执行,fourth先于third打印
NSLog(@"fourth");
}
- (void)secondMethod {
NSLog(@"second");
}
- (void)thirdMethod {
NSLog(@"third");
}
打印结果:
2019-05-13 15:13:23.519717+0800 Mytest[70210:3349924] first
2019-05-13 15:13:23.519875+0800 Mytest[70210:3349924] second
2019-05-13 15:13:23.520021+0800 Mytest[70210:3349924] fourth
2019-05-13 15:13:24.021104+0800 Mytest[70210:3349924] third
3)performSelector的其他使用方法:
a. 建立动态的函数,然后调用他们
- (void)viewDidLoad {
[super viewDidLoad];
NSArray *methodArr = @[@{@"methodName":@"methodOne",@"firParValue":@"one",@"secondParName":@"parTwo",@"secondParValue":@"valueTwo"},@{@"methodName":@"methodTwo",@"firParValue":@1,@"secondParName":@"secondPar",@"secondParValue":@2}];
for (NSDictionary *d in methodArr) {
SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@:%@:",d[@"methodName"],d[@"secondParName"]]);
if ([self respondsToSelector:sel]) {
[self performSelector:sel withObject:d[@"firParValue"] withObject:d[@"secondParValue"]];
}
}
}
- (void)methodOne:(NSString *)par parTwo:(NSString *)parTwo{
NSLog(@"par1 = %@,parTwo = %@",par,parTwo);
}
- (void)methodTwo:(NSNumber *)par secondPar:(NSNumber *)secondPar{
NSLog(@"par2 = %@,secondPar = %@",par,secondPar);
}
打印结果:
2019-05-13 15:47:05.147742+0800 Mytest[70573:3364564] par1 = one,parTwo = valueTwo
2019-05-13 15:47:05.147918+0800 Mytest[70573:3364564] par2 = 1,secondPar = 2
b. 防止按钮多次点击
方案一:通过UIButton
的enabled
属性和userInteractionEnabled
属性控制按钮是否可点击。
方案二:通过NSObjec
t的+cancelPreviousPerformRequestsWithTarget:selector:object:
方法和-performSelector:withObject:afterDelay:
方法控制按钮的响应时间的执行时间间隔。此方案会在连续点击按钮时取消之前的点击事件,从而只执行最后一次点击事件,会出现延迟现象。
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *testBtn = [UIButton buttonWithType:UIButtonTypeCustom];
testBtn.backgroundColor = [UIColor redColor];
testBtn.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:testBtn];
[testBtn addTarget:self action:@selector(testClick:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)testClick:(UIButton *)sender {
NSLog(@"test");
// 方案一
// sender.enabled = NO;
//// [self performSelector:@selector(changeBtnStatus:) withObject:sender afterDelay:0.2f];
//或者
// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// sender.enabled = YES;
// });
// 方案二
[[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(btnClick:) object:sender];
[self performSelector:@selector(btnClick:) withObject:sender afterDelay:0.2f];
}
- (void)changeBtnStatus:(UIButton *)sender {
sender.enabled = YES;
}
- (void)btnClick:(UIButton *)sender {
NSLog(@"点击了我");
}
方案三:通过runtime
控制UIButton
响应事件的时间间隔,最优解
通过给UIButton分类替换方法,添加属性实现
#import "UIButton+EventInterval.h"
#import "objc/message.h"
static char * const eventIntervalKey = "eventIntervalKey";
static char * const eventUnavailableKey = "eventUnavailableKey";
@interface UIButton ()
@property (nonatomic, assign) BOOL eventUnavailable;
@end
@implementation UIButton (EventInterval)
+ (void)load {
//这个方法是处理点击事件的
Method method = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
//新方法
Method newMethod = class_getInstanceMethod(self, @selector(newSendAction:to:forEvent:));
//交换两个方法的具体实现
method_exchangeImplementations(method, newMethod);
// NSLog(@"self.eventUnavailable = %d",self.eventUnavailable);
}
//新的方法
-(void)newSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if (self.eventUnavailable == NO) {//调用get方法
self.eventUnavailable = YES;//调用set方法
[self newSendAction:action to:target forEvent:event];
[self performSelector:@selector(setEventUnavailable:) withObject:@(NO) afterDelay:self.myEventInterval];
}
}
- (NSTimeInterval)myEventInterval {
return [objc_getAssociatedObject(self, eventIntervalKey) doubleValue];
}
- (void)setMyEventInterval:(NSTimeInterval)myEventInterval {
objc_setAssociatedObject(self, eventIntervalKey, @(myEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//关联属性
//通过关联属性对分类增加属性
//get方法实现
- (BOOL)eventUnavailable {
return [objc_getAssociatedObject(self, eventUnavailableKey) doubleValue];
}
//set方法实现
- (void)setEventUnavailable:(BOOL)eventUnavailable {
objc_setAssociatedObject(self, eventUnavailableKey, @(eventUnavailable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
使用:
UIButton *testBtn = [UIButton buttonWithType:UIButtonTypeCustom];
testBtn.myEventInterval = 0.5f;//这句
testBtn.backgroundColor = [UIColor redColor];
testBtn.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:testBtn];
[testBtn addTarget:self action:@selector(testClick:) forControlEvents:UIControlEventTouchUpInside];
关联对象
我们都知道分类是不能自定义属性和变量的,但是可以通过关联对象实现给分类添加属性。
//关联对象
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:
内存管理的策略
关于runtime的整理结束,如果有补充的欢迎进行留言。未完待续…………
下一篇:关于成员变量
,实例变量
,@public
,@private
,@protected
,@package
,@property
,@synthesize
,@dynamic
点击这里跳转