面试题

Runtime相关问题

2020-03-30  本文已影响0人  6ffd6634d577
1. 介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)

objc4-779源码

首先,关于NSObject,objc_class 和 objc_object 三者之间的关系,我们可以用下面的图来更清晰的了解:


image.png
一 NSObject

NSObject是OC 中的基类,除了NSProxy其他都继承自NSObject

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
二 对象结构体 objc_object

在运行时,类的对象被定义为objc_object类型,就是对象结构体,在OC 中每一个对象都是一个结构体,结构体都包含了一个isa成员变量。根据isa的定义可以知道,类型为isa_t类型的

struct objc_object {
private:
    isa_t isa;

public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();

    // 省略其余方法
    ...
}

isa_t 的定义是什么?
是一个union的结构对象,类似于C++结构体,其内部可以定义为成员变量和函数,是一个联合类型,其中的 isa_t、cls、 bits 还有结构体共用同一块地址空间。

三 类结构体 objc_class

类也是一个对象,类的结构体objc_class 是继承自objc_object的,具备对象的所有特征

iOS中不管类对象还是元类对象类型都是Class,而Class的结构则是objc_class结构体

typedef struct objc_class *Class;
typedef struct objc_object *id;

//注意,有些人看到的objc_class结构体定义不一样,有OBJC2_UNAVAILABLE 的注释,在OC 2.0中,
//这种关于之前objc_class的定义已经废弃掉了,可以在 objc-runtime-new.h 看OC 2.0之后的,如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // 方法缓存  formerly cache pointer and vtable 
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ...//此处省略
}

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;
    private:
    ...
 
    public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData)
    {
        assert(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
        // Set during realization or construction only. No locking needed.
        // Use a store-release fence because there may be concurrent
        // readers of data and data's contents.
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    }
    ...

cache_t cache

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;

public:
    struct bucket_t *buckets();
    mask_t mask();
    mask_t occupied();
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty();

    mask_t capacity();
    bool isConstantEmptyCache();
    bool canBeFreed();

    static size_t bytesForCapacity(uint32_t cap);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);

    void expand();
    void reallocate(mask_t oldCapacity, mask_t newCapacity);
    struct bucket_t * find(cache_key_t key, id receiver);

    static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
image.png

根据源码,我们可以知道 cache_t 中存储了一个 bucket_t 的结构体,和两个unsigned int的变量。
mask:分配用来缓存bucket的总数。
occupied:表明目前实际占用的缓存bucket的个数。

bucket_t 的结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。

cache_t 中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。

Cache 的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

class_data_bits_t bits

存储类的方法、属性、遵循的协议等信息的地方

class_data_bits_t bits有一个成员uintptr_t bits, 可以理解为一个‘复合指针’。什么意思呢,就是bits不仅包含了指针,同时包含了Class的各种异或flag,来说明Class的属性。把这些信息复合在一起,仅用一个uint指针bits来表示。当需要取出这些信息时,需要用对应的以FAST_前缀开头的flag掩码bits按位与操作。

例如,我们需要取出Classs的核心信息class_rw_t, 则需要调用方法:

    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
image.png image.png
2. 为什么要设计metaclass

类对象(class object)中包含了类的实例变量,实例方法的定义,
元类对象(metaclass object)中包括了类对象的方法,也就是类方法(也就是C++中的静态方法)的定义。

那么可不可以把元类干掉,在类中把实例方法和类方法存在两个不同的数组中?

__class_lookupMethodAndLoadCache3

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
`lookUpImpOrForward`用于查找方法
1、首先会再一次的从类中寻找需要调用方法的缓存,如果能命中缓存直接返回该方法的实现,如果不能命中则继续往下走。
2、从类的方法列表中寻找该方法,如果能从列表中找到方法则对方法进行缓存并返回该方法的实现,如果找不到该方法则继续往下走。
3、从父类的缓存寻找该方法,如果父类缓存能命中则将方法缓存至当前调用方法的类中(注意这里不是存进父类),如果缓存未命中则遍历父类的方法列表,之后操作如同第2步,未能命中则继续走第3步直到寻找到基类。
4、如果到基类依然没有找到该方法则触发动态方法解析流程。=
5、还是找不到就触发消息转发流程

答:行是肯定可行的,但是在lookUpImpOrForward执行的时候就得标注上传入的cls到底是实例对象还是类对象,这也就意味着在查找方法的缓存时同样也需要判断cls到底是个啥。

从OC的消息机制分析了元类存在的意义,元类的存在巧妙的简化了实例方法和类方法的调用流程,大大提升了消息发送的效率

metaclass代表的是类对象的对象,它存储了类的类方法,它的目的是将实例和类的相关方法列表以及构建信息区分开来,方便各司其职,符合单一职责设计原则。

具体可以参考这篇文章

3. class_copyIvarList & class_copyPropertyList区别

class_copyIvarList

获取 类对象 中的所有实例变量信息,从 class_ro_t 中获取

class_copyPropertyList

获取 类对象 中的属性信息, class_rw_tproperties,先后输出了 category / extension/ baseClass 的属性,而且仅输出 当前的类 的属性信息,而不会向上去找 superClass 中定义的属性。

Q1: class_ro_t 中的 baseProperties 呢?
Q2: class_rw_t 中的 properties 包含了所有属性,那何时注入进去的呢?
An: methodizeClass 方法,会把类里面的属性,协议,方法都加载进来

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
4. class_rw_t 和 class_ro_t 的区别
image.png
class_rw_t 表示read write,class_ro_t 表示 read only

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;

    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

每个类都对应有一个class_ro_t结构体和一个class_rw_t结构体。在编译期间class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址

区别在于:class_ro_t存放的是编译期间就确定的属性、方法和遵守协议;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容

OC对象中存储的属性、方法、遵循的协议数据其实被存储在这两块儿内存区域的,而我们通过runtime动态修改类的方法时,是修改在class_rw_t区域中存储的方法列表。

参考这篇文章
更加详细的分析,请看@Draveness 的这篇文章深入解析 ObjC 中方法的结构

5. category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序

objc_init ->... -> realizeClass -> methodizeClass(用于Attach categories)-> attachCategories -> attachLists

在运行时调用 realizeClass方法,会做以下3件事情:

  1. class_data_bits_t调用 data方法,将结果从 class_rw_t强制转换为 class_ro_t指针
  2. 初始化一个 class_rw_t结构体
  3. 设置结构体 ro的值以及 flag

最后调用methodizeClass方法,把分类里面的属性,协议,方法都加载进来。
关键就是在methodizeClass 方法实现中

category的加载是在运行时发生的,加载过程是,把category的实例方法、属性、协议添加到类对象上。把category的类方法、属性、协议添加到metaclass上。

category的load方法执行顺序是根据类的编译顺序决定的,即:xcode中的Build Phases中的Compile Sources中的文件从上到下的顺序加载的。

category并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且category添加的methodA会排在原有类的methodA的前面,因此如果存在category的同名方法,那么在调用的时候,则会先找到最后一个编译的 category里的对应方法

6、category & extension区别,能给NSObject添加Extension吗,结果如何?

category:分类

注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。

extension:扩展

不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的extension。

8. 在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么

OC中的方法调用,编译后的代码最终都会转成objc_msgSend(id , SEL, ...)方法进行调用,这个方法第一个参数是一个消息接收者对象,runtime通过这个对象的isa指针找到这个对象的类对象,从类对象中的cache中查找是否存在SEL对应的IMP,若不存在,则会在 method_list中查找,如果还是没找到,则会到supper_class中查找,仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)进行消息转发。

9. IMP、SEL、Method的区别和使用场景
typedef struct objc_selector *SEL

SEL: 方法选择器,虽然 SELobjc_selector 结构体指针,但实际上它只是一个 C 字符串,方法的名称

typedef id (*IMP)(id, SEL, …)

IMP-函数指针,指向实际执行函数体

typedef struct objc_method *Method

/// Method
struct objc_method {
    SEL method_name; 
    char *method_types;
    IMP method_imp;
};

方法名 method_name 类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
方法类型 method_types 是个 char 指针,其实存储着方法的参数类型返回值类型,即是 Type Encoding 编码。
method_imp 指向方法的实现,本质上是一个函数的指针,就是前面讲到的 Implementation。

使用场景:
实现类的swizzle的时候会用到,通过class_getInstanceMethod(class, SEL)来获取类的方法Method,其中用到了SEL作为方法名
调用method_exchangeImplementations(Method1, Method2)进行方法交换

我们还可以给类动态添加方法,此时我们需要调用class_addMethod(Class, SEL, IMP, types),该方法需要我们传递一个方法的实现函数IMP,例如:

static void funcName(id receiver, SEL cmd, 方法参数...) {
   // 方法具体的实现   
}

函数第一个参数:方法接收者,第二个参数:调用的方法名SEL,方法对应的参数,这个顺序是固定的。

10、load、initialize方法的区别什么?在继承关系中他们有什么区别

load:当类被装载的时候被调用,只调用一次

initialize:当类或子类第一次收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),只调用一次

参考这篇文章

11. _objc_msgForward函数是做什么的,直接调用它将会发生什么?

_objc_msgForward是一个函数指针(和 IMP 的类型一样),是用于消息转发的:
当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发

也就是说_objc_msgForward在进行消息转发的过程中会涉及以下这几个方法:

resolveInstanceMethod:方法 (或 resolveClassMethod:)。
forwardingTargetForSelector:方法
methodSignatureForSelector:方法
forwardInvocation:方法
doesNotRecognizeSelector: 方法

12. 通过runtime动态创建一个类

要创建一个新类,

  1. 首先调用objc_allocateClassPair
  2. 然后使用class_addMethodclass_addIvar等函数设置类的属性。
  3. 完成构建类后调用objc_registerClassPair
/** 
 * 创建一个新类和元类.
 * 
 * @param superclass 这个类是新创建的类的父类,可以传入Nil去创建一个新根类.
 * @param name 这个字符串是类的名字(例:"NSObject")
 * @param extraBytes 一般传入0 
 * @return 新的类,如果返回的是Nil,那么就是这个类创建失败了(例:创建的是"NSObject"类,然而这个类已经存在了)
 */
objc_allocateClassPair(
Class _Nullable superclass, 
const char * _Nonnull name,  
size_t extraBytes
) 

objc_allocateClassPair只返回一个值:Class。那么pair的另一半在哪里呢?
是的,估计你已经猜到了这个另一半就是meta-class

/** 
 * 注册使用`objc_allocateClassPair`方法创建的类
 * 
 * @param cls 需要注册的类(不能为Nil)
 */ 
objc_registerClassPair(Class _Nonnull cls) 

代码样例

/// 创建一个元类
Class class = objc_allocateClassPair([NSObject class], "Person", 0);
/// 添加方法
//class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>, <#IMP  _Nonnull imp#>, <#const char * _Nullable types#>)
/// 添加属性
//class_addIvar(<#Class  _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
/// 注册元类
objc_registerClassPair(class);
13. object_getClass 和 object_setClass

1. Class object_getClass(id obj)

    //返回对象的isa指针
     eg:参数为实例对象返回类对象
     eg:参数为类对象返回元类对象
     eg:参数为元类对象返回根元类对象
     eg:参数为根元类对象返回自身

    Class object_getClass(id obj)

   // 根据字符串返回类对象
    Class _Nullable objc_getClass(const char * _Nonnull name)
    // 创建一个NSObject对象obj,然后获取obj的类
    NSObject *obj = [[NSObject alloc] init]; // 1
    Class objClass = object_getClass(obj); // 2
    NSLog(@"%@", NSStringFromClass(objClass)); // 3
    
    Class nsobjectClass = object_getClass([NSObject class]); // 4
    NSLog(@"%@", NSStringFromClass(nsobjectClass)); // 5
    
    BOOL isMeta1 = class_isMetaClass(objClass); // 6
    BOOL isMeta2 = class_isMetaClass(nsobjectClass); // 7
    NSLog(@"isMeta1:%i, isMeta2:%i", isMeta1, isMeta2); // 8

//打印结果
2020-04-03 18:08:48.875921+0800 ImplementKVO[81240:4259330] NSObject
2020-04-03 18:08:48.876057+0800 ImplementKVO[81240:4259330] NSObject
2020-04-03 18:08:48.876155+0800 ImplementKVO[81240:4259330] isMeta1:0, isMeta2:1

说明:class_isMetaClass函数的作用是判断传入的类是不是元类,经过判断是不是元类可看出区别,因此可得出结论:
object_getClass函数的参数传一个类的实例时,返回的是该实例的类对象
参数传时,返回的是该类的元类

    Class class1 = [obj class];
    Class class2 = [[NSObject class] class];

    NSLog(@"%@", NSStringFromClass(class1));
    NSLog(@"%@", NSStringFromClass(class2));
    NSLog(@"%i, %i", class_isMetaClass(class1), class_isMetaClass(class2));

2020-04-03 18:17:05.175123+0800 ImplementKVO[81544:4270627] NSObject
2020-04-03 18:17:05.175261+0800 ImplementKVO[81544:4270627] NSObject
2020-04-03 18:17:05.175363+0800 ImplementKVO[81544:4270627] 0, 0

通过打印结果可以看出,class方法的调用者是一个实例时,获取到的是该实例的类,此时和object_getClass函数作用相同;而调用者是一个(比如[NSObject class])时,获取到的并不是该类的元类,此时和object_getClass函数的作用不同.

//[xxx class]方法内部实现

+ (Class)class {
return self;
}

- (Class)class {
return object_getClass(self);
}

2. Class object_setClass(id obj, Class cls)

/**
将一个对象设置为别的类类型,返回原来的Class ,将一个对象的isa指针指向设置的Class
 * Sets the class of an object.
 * 
 * @param obj The object to modify.
 * @param cls A class object.
 * 
 * @return The previous value of \e object's class, or \c Nil if \e object is \c nil.
 */

Class _Nullable  object_setClass(id _Nullable obj, Class _Nonnull cls) 


    // 分别创建一个可变数组对象mutArray和不可变数组对象array
    NSMutableArray *mutArray = [NSMutableArray arrayWithObjects:@"a", @"b", nil]; // 1
    NSArray *array = @[@"c", @"d"]; // 2

    // 获取这两个对象mutArray和array的类并打印
    Class mutArrayClassBefore = object_getClass(mutArray); // 3
    Class arrayClassBefore = object_getClass(array); // 4
    NSLog(@"%@ -- %@", NSStringFromClass(mutArrayClassBefore), NSStringFromClass(arrayClassBefore)); // 5

    Class setclass = object_setClass(mutArray, arrayClassBefore); // 6
    NSLog(@"%@", NSStringFromClass(setclass)); // 7

    Class mutArrayClassNow = object_getClass(mutArray); // 8
    Class arrayClassNow = object_getClass(array); // 9

    NSLog(@"%@ -- %@", NSStringFromClass(mutArrayClassNow), NSStringFromClass(arrayClassNow)); // 10

2020-04-03 18:27:43.569594+0800 ImplementKVO[81775:4277728] __NSArrayM -- __NSArrayI
2020-04-03 18:27:43.569724+0800 ImplementKVO[81775:4277728] __NSArrayM
2020-04-03 18:27:43.569822+0800 ImplementKVO[81775:4277728] __NSArrayI -- __NSArrayI

从打印结果可以看出用object_setClass函数将可变数组对象mutArray的类设置为它的父类是可以的,此时再用mutArray调用NSMutableArray的方法会导致程序crash,如

[mutArray addObject:@"e"]; // 11

crash信息为经典的:

2020-04-03 18:31:04.101045+0800 ImplementKVO[81860:4280357] -[__NSArrayI addObject:]: unrecognized selector sent to instance 0x6000000fec40

总结

14. Method Swizzle注意事项
  1. 对于一般的Class.推荐在load方法中交换, 系统的类,可以通过Category添加方法交换
  2. 避免交换父类方法(先class_addMethod,判断是否成功)
  3. 交换方法应在+load方法
  4. 交换方法应该放到dispatch_once中执行
  5. 交换的分类方法应该添加自定义前缀,避免冲突
  6. 交换的分类方法应调用原实现

注意:如果是交换不同类的方法,并且在方法中访问了类的属性,会造成Crash,更安全的做法,Runtime 还提供了另一种 Swizzle 函数 method_setImplementation。
method_setImplementation 可以让我们提供一个新的函数来代替我们要替换的方法。 而不是将两个方法的实现做交换。 这样就不会造成 method_exchangeImplementations 的潜在对已有实现的副作用了。
method_setImplementation 接受两个参数,第一个还是我们要替换的方法, 而第二个参数是一个 IMP 类型的。

Objc 黑科技 - Method Swizzle 的一些注意事项

例如:统计VC加载次数并打印

#import "UIViewController+Logging.h"
#import <objc/runtime.h>

@implementation UIViewController (Logging)

+ (void)load
{
    swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}

- (void)swizzled_viewDidAppear:(BOOL)animated
{
  //交换前需要先调用系统的方法初始化调用[self xxx]的系统方法的时候.需要把xxx改成xx_xxx(你交换的方法)
  //这里是因为你的方法已经和系统的方法交换了,调用你的方法其实是调用的系统方法,调用系统方法的话就调用的是你的方法,
  //然后就会产生循环调用.
    // call original implementation
    [self swizzled_viewDidAppear:animated];
    
    // Logging
    NSLog(@"%@", NSStringFromClass([self class]));
}

void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
    // the method might not exist in the class, but in its superclass
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // class_addMethod will fail if original method already exists
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
    // the method doesn’t exist and we just added one
    if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }
    else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    
}

参考文章

15. iOS 中内省的几个方法有哪些?内部实现原理是什么

反射机制

16. 怎么防止UI控件短时间多次激活事件?

需求

当前项目写好的按钮,还没有全局地控制他们短时间内不可连续点击(也许有过零星地在某些网络请求接口之前做过一些控制)。现在来了新需求:本APP所有的按钮1秒内不可连续点击。你怎么做?一个个改?这种低效率低维护度肯定是不妥的。

方案

给按钮添加分类,并添加一个点击事件间隔的属性,执行点击事件的时候判断一下是否时间到了,如果时间不到,那么拦截点击事件。
怎么拦截点击事件呢?其实点击事件在runtime里面是发送消息,我们可以把要发送的消息的SEL 和自己写的SEL交换一下,然后在自己写的SEL里面判断是否执行点击事件。

实践

UIButton是UIControl的子类,因而根据UIControl新建一个分类即可

#import "UIControl+Limit.h"
#import <objc/runtime.h>

static const char *UIControl_acceptEventInterval="UIControl_acceptEventInterval";
static const char *UIControl_ignoreEvent="UIControl_ignoreEvent";

@implementation UIControl (Limit)

#pragma mark - acceptEventInterval
- (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval
{
    objc_setAssociatedObject(self,UIControl_acceptEventInterval, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSTimeInterval)acceptEventInterval {
    return [objc_getAssociatedObject(self,UIControl_acceptEventInterval) doubleValue];
}

#pragma mark - ignoreEvent
-(void)setIgnoreEvent:(BOOL)ignoreEvent{
    objc_setAssociatedObject(self,UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
}

-(BOOL)ignoreEvent{
    return [objc_getAssociatedObject(self,UIControl_ignoreEvent) boolValue];
}

#pragma mark - Swizzling
+(void)load {
    Method a = class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
    Method b = class_getInstanceMethod(self,@selector(swizzled_sendAction:to:forEvent:));
    method_exchangeImplementations(a, b);//交换方法
}

- (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event
{
    if(self.ignoreEvent){
        NSLog(@"btnAction is intercepted");
        return;
    }
    if(self.acceptEventInterval>0){
        self.ignoreEvent=YES;
        //延迟执行
        [self performSelector:@selector(setIgnoreEventWithNo)  withObject:nil afterDelay:self.acceptEventInterval];
    }
    //调用原方法实现
    [self swizzled_sendAction:action to:target forEvent:event];
}

-(void)setIgnoreEventWithNo{
    self.ignoreEvent=NO;
}

@end

-(void)setupSubViews{
    
    UIButton *btn = [UIButton new];
    btn =[[UIButton alloc]initWithFrame:CGRectMake(100,100,100,40)];
    [btn setTitle:@"btnTest"forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor redColor]forState:UIControlStateNormal];
    btn.acceptEventInterval = 3;
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside];
}

- (void)btnAction{
    NSLog(@"btnAction is executed");
}

//日志打印
2020-04-06 21:07:44.244251+0800 Runtime[5230:326947] btnAction is executed
2020-04-06 21:07:45.077352+0800 Runtime[5230:326947] btnAction is intercepted
2020-04-06 21:07:45.871633+0800 Runtime[5230:326947] btnAction is intercepted
2020-04-06 21:07:46.844650+0800 Runtime[5230:326947] btnAction is intercepted
2020-04-06 21:07:47.719857+0800 Runtime[5230:326947] btnAction is executed
17. 防奔溃处理

崩溃拦截

18. AOP(切面编程)

参考总结

上一篇 下一篇

猜你喜欢

热点阅读