iOS知识点整理-Runtime

前言:我是一个有着两年开发经验的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:

  1. sel_registerName 函数
  2. Objective-C 编译器提供的 @selector() 方法
  3. 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);

消息传递流程:

  1. 当消息传递给一个对象时,首先从运行时系统缓存 objc_cache 中进行查找。如果找到,则执行。否则,继续执行下面步骤。
  2. objc_msgSend通过对象的isa指针获取到类的结构体;
  3. 在方法分发表method_list查找,是否有与选择器名称相符的selector方法;
  4. 没有则根据所属类的superClass指针,沿着类的继承体系继续向上查找;
  5. 有则根据IMP指针跳转到方法的实现代码,传入参数调用这个方法的实现,并将该方法加入缓存objc_cache中;
  6. 转发IMP的return值
  7. 如果在继承体系中还是没找到与选择器相符的方法,此时就会执行“消息转发”(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当前类的子类,并为这个新的子类重写了被观察属性keypathsetter方法。setter方法随后负责通知观察对象属性的改变状况。Apple使用了isa-swizzling来实现KVO,当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotififying_A的新类,该类继承自对象A的本类,且KVONSKVONotifying_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. 防止按钮多次点击

方案一:通过UIButtonenabled属性和userInteractionEnabled属性控制按钮是否可点击。
方案二:通过NSObject的+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点击这里跳转

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342