Runtime相关问题

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:分类

  • 给类添加新的方法
  • 不能给类添加成员变量
  • 通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和* setter方法,同时也不能生成带下划线的成员属性
  • 是运行期决定的

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

extension:扩展

  • 可以给类添加成员变量,但是是私有的
  • 可以給类添加方法,但是是私有的
  • 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
  • 伴随着类的产生而产生,也随着类的消失而消失
  • 必须有类的源码才可以给类添加extension,所以对于系统一些类,如nsstring,就无法添加类扩展

不能给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:当类被装载的时候被调用,只调用一次

  • 调用方式并不是采用runtime的objc_msgSend方式调用的,而是直接采用函数的内存地址直接调用的
  • 多个类的load调用顺序,是依赖于compile sources中的文件顺序决定的,根据文件从上到下的顺序调用
  • 子类和父类同时实现load的方法时,父类的方法先被调用
  • 本类与category的调用顺序是,优先调用本类的(注意:category是在最后被装载的)
  • 多个category,每个load都会被调用(这也是load的调用方式不是采用objc_msgSend的方式调用的),同样按照compile sources中的顺序调用的
  • load是被动调用的,在类装载时调用的,不需要手动触发调用

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

  • 调用方式是通过runtime的objc_msgSend的方式调用的,此时所有的类都已经装载完毕
  • 子类和父类同时实现initialize,父类的先被调用,然后调用子类
  • 本类与category同时实现initialize,category覆盖本类的方法,只调用category的initialize一次(这也说明initialize的调用方式采用objc_msgSend的方式调用的)
  • 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

总结

  • 使用object_setClass后会使对象的isa的值指向新的Class
  • 使用object_setClass对象的内存布局不会发生变化。
  • 使用object_setClass不能访问超过原对象申请的内存区域,否则程序会crash
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加载次数并打印

  • UIViewController+Logging.m
#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新建一个分类即可

  • UIControl+Limit.m
#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

  • ViewController.m
-(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(切面编程)

参考总结

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

推荐阅读更多精彩内容