Runtime详解整理(Runtime 相关数据结构、Runtime消息传递机制、Runtime消息转发机制、Runtime的应用)

Runtime

Runtime是什么?

我们都知道,将源代码转换为可执行的程序,通常要经过三个步骤:编译、链接、运行。不同的编译语言,在这三个步骤中所进行的操作又有些不同。

C 语言 作为一门静态类语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了要调用的函数,以及函数的实现。

Objective-C 语言 是一门动态语言。在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的哪个函数。只有在运行时间才检查变量的数据类型,同时在运行时才会根据函数名查找要调用的具体函数。这样在程序没运行的时候,我们并不知道调用一个方法具体会发生什么。

Objective-C 语言 把一些决定性的工作从编译阶段、链接阶段推迟到 运行时阶段 的机制,使得 Objective-C 变得更加灵活。我们甚至可以在程序运行的时候,动态的去修改一个方法的实现,这也为大为流行的『热更新』提供了可能性。

而实现 Objective-C 语言 运行时机制 的一切基础就是 Runtime

Runtime 实际上是一个库,这个库使我们可以在程序运行时动态的创建对象、检查对象,修改类和对象的方法

#import <objc/runtime.h>
#import <objc/message.h>

  • 运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时
  • Runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API
  • Object-C需要Runtime来创建类和对象,进行消息发送和转发

Runtime 相关数据结构

在这里插入图片描述

Object(对象)

接下来,我们再来看看 objc/objc.h 中关于 Object(对象) 的定义。
Object(对象)被定义为 objc_object 结构体,其数据结构如下:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa;       // objc_object 结构体的实例指针
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

这里的 id 被定义为一个指向 objc_object 结构体 的指针。从中可以看出 objc_object 结构体 只包含一个 Class 类型的 isa 指针。

换句话说,一个 Object(对象)唯一保存的就是它所属 Class(类) 的地址。当我们对一个对象,进行方法调用时,比如 [receiver selector];,它会通过 objc_object 结构体的 isa 指针 去找对应的 object_class 结构体,然后在 object_class 结构体 的 methodLists(方法列表) 中找到我们调用的方法,然后执行。

isa_t,关于isa操作相关,弱引用相关,关联对象相关,内存管理相关

Class(类)

在 objc/runtime.h 中,Class(类) 被定义为指向 objc_class 结构体 的指针,objc_class 结构体 的数据结构如下:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
struct objc_class {
    Class _Nonnull isa;                     // objc_class 结构体的实例指针

#if !__OBJC2__
    Class _Nullable super_class;             // 指向父类的指针
    const char * _Nonnull name;              // 类的名字
    long version;                            // 类的版本信息,默认为 0
    long info;                               // 类的信息,供运行期使用的一些位标识
    long instance_size;                      // 该类的实例变量大小;
    struct objc_ivar_list * _Nullable ivars;  // 该类的实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists; // 方法定义的列表
    struct objc_cache * _Nonnull cache;                   // 方法缓存
    struct objc_protocol_list * _Nullable protocols;     // 遵守的协议列表
#endif
};

从中可以看出,objc_class 结构体 定义了很多变量:自身的所有实例变量(ivars)、所有方法定义(methodLists)、遵守的协议列表(protocols)等。objc_class 结构体 存放的数据称为 元数据(metadata)

objc_class 结构体 的第一个成员变量是 isa 指针,isa 指针 保存的是所属类的结构体的实例的指针,这里保存的就是 objc_class 结构体的实例指针,而实例换个名字就是 对象。换句话说,Class(类) 的本质其实就是一个对象,我们称之为 类对象

Meta Class(元类)

从上边我们看出,对象(objc_object 结构体) 的 isa 指针 指向的是对应的 类对象(object_class 结构体)。那么 类对象(object_class 结构体)的 isa 指针 又指向什么呢?

object_class 结构体 的 isa 指针 实际上指向的的是 类对象 自身的 Meta Class(元类)

那么什么是 Meta Class(元类)?

Meta Class(元类) 就是一个类对象所属的 类。一个对象所属的类叫做 类对象,而一个类对象所属的类就叫做 元类。

Runtime 中把类对象所属类型就叫做 Meta Class(元类),用于描述类对象本身所具有的特征,而在元类的 methodLists 中,保存了类的方法链表,即所谓的「类方法」。并且类对象中的 isa 指针 指向的就是元类。每个类对象有且仅有一个与之相关的元类

消息机制的基本原理 中我们讲解了 对象方法的调用过程,我们是通过对象的 isa 指针 找到 对应的 Class(类);然后在 Class(类) 的 method list(方法列表) 中找对应的 selector 。

类方法的调用过程 和对象方法调用差不多,流程如下:
通过类对象 isa 指针 找到所属的 Meta Class(元类);
在 Meta Class(元类) 的 method list(方法列表) 中找到对应的 selector;
执行对应的 selector。

对象,类对象,元类对象 之间的关系

类对象存储实例方法列表等信息。
元类对象存储类方法列表等信息。

在这里插入图片描述
isa Class对象,指向objc_class结构体的指针,也就是这个Class的MetaClass(元类)

  • 类的实例对象的 isa 指向该类;该类的 isa 指向该类的 MetaClass
  • MetaCalss的isa对象指向RootMetaCalss
  • 如果MetaClass是RootMetaCalss,那么该MetaClass的isa指针指向它自己
    super_class Class对象指向父类对象
  • 如果该类的对象已经是RootClass,那么这个super_class指向nil
  • MetaCalss的SuperClass指向父类的MetaCalss
  • MetaCalss是RootMetaCalss,那么该MetaClass的SuperClass指向该对象的RootClass
isa指针,共用体isa_t

在这里插入图片描述
isa指向

  • 关于对象,其指向类对象。
  • 关于类对象,其指向元类对象。
  • 实例–(isa)–>class–(isa)–>MetaClass
cache_t

用于快速查找方法执行函数,是可增量扩展的哈希表结构,是局部性原理的最佳运用
主要用于缓存常用方法列表,每个类中有很多方法,我平时不用的方法也会在里面,每次运行一个方法,都要去methodLists遍历得到方法,如果类的方法不多还行,但是基本的类中都会有很多方法,这样势必会影响程序的运行效率,所以cache在这里就会被用上,当我们使用这个类的方法时先判断cache是否为空,为空从methodLists找到调用,并保存到cache,不为空先从cache中找方法,如果找不到在去methodLists,这样提高了程序方法的运行效率

当Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。

 struct cache_t {
    struct bucket_t *_buckets;//一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
    mask_t _mask;//分配用来缓存bucket的总数
    mask_t _occupied;//表明目前实际占用的缓存bucket的个数struct bucket_t {
    private:
    cache_key_t _key;
    IMP _imp;
class_data_bits_t:对class_rw_t的封装
struct class_rw_t {
     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;

Objc的类的属性、方法、以及遵循的协议都放在class_rw_t中,class_rw_t代表了类相关的读写信息,是对class_ro_t的封装,而class_ro_t代表了类的只读信息,存储了 编译器决定了的属性、方法和遵守协议

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;
    }
  };
method_t

函数四要素:名称,返回值,参数,函数体

struct method_t {
  SEL name;           //名称
  const char *types;//返回值和参数
  IMP imp;              //函数体
protocols

故名思义,这个类中都遵守了哪些协议,使用场景:判断类是否遵守了某个协议上

SEL(objc_selector)

selector是方法选择器
selector是SEL的一个实例

 Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;
IMP

就是指向最终实现程序的内存地址的指针。
在iOS的Runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

/// A pointer to the function of a method implementation.  指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...); 
#endif

runtime是如何通过selector找到对应的IMP地址的?
缓存查找–>当前类查找–>父类逐级查找

Category(objc_category)
struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。

Runtime消息传递

Objective-C 语言 中,对象方法调用都是类似 [receiver selector]; 的形式,其本质就是让对象在运行时发送消息的过程。

我们来看看方法调用 [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,消息被转发或者临时向 recever 添加这个 selector 对应的实现方法,否则就会发生崩溃。

消息传递的流程:缓存查找–>当前类查找–>父类逐级查找

  • 调用方法之前,先去查找缓存,看看缓存中是否有对应选择器的方法实现,如果有,就去调用函数,完成消息传递(缓存查找:给定值SEL,目标是查找对应bucket_t中的IMP,哈希查找)
  • 如果缓存中没有,会根据当前实例的isa指针查找当前类对象的方法列表,看看是否有同样名称的方法,如果找到,就去调用函数,完成消息传递(当前类中查找:对于已排序好的方法列表,采用二分查找,对于没有排序好的列表,采用一般遍历)
  • 如果当前类对象的方法列表没有,就会逐级父类方法列表中查找,如果找到,就去调用函数,完成消息传递(父类逐级查找:先判断父类是否为nil,为nil则结束,否则就继续进行缓存查找–>当前类查找–>父类逐级查找的流程)
  • 如果一直查到根类依然没有查找到,则进入到消息转发流程中,完成消息传递

一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的:

  1. 系统首先找到消息的接收对象,然后通过对象的isa找到它的类。
  2. 在它的类中查找method_list,是否有selector方法。
  3. 没有则查找父类的method_list。
  4. 找到对应的method,执行它的IMP。
  5. 转发IMP的return值。

消息机制就是向接收者发送消息,并带有参数,根据接收者对象的数据结构,找到相关发放实现,最后达到这个消息的目的,

objc_msgSend是Runtime的核心,Objective-C中调用对象方法就是消息传递。

objc_msgSend并不是直接调用方法实现(IMP)而是发送消息,让类的结构体去动态查到方法实现,所以在为查找到方法实现之前我们可以动态的去修改这个方法的实现

Runtime 消息转发

  • (BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
  • (BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
  • (id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
  • (void)forwardInvocation:(NSInvocation *)anInvocation;
    在这里插入图片描述
    那么最后消息未能处理的时候,还会调用到
  • (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来
动态方法解析(消息动态解析)

首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。
注意:
当这个对象在实现了resolveInstanceMethod,resolveClassMethod两个方法,并没有对该对象消息进行处理,那么该方法会被调用两次:

一次是没有找到该方法需要对象解析处理;第二次是告诉系统我处理完成需要再次调用该方法但实际上并没有处理完成,所以会调用第二次该方法崩溃

实现一个动态方法解析的例子如下

// 类方法未找到时调起,可以在此添加类方法实现
//+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此对象方法实现
//+ (BOOL)resolveInstanceMethod:(SEL)sel;

/** 
 * class_addMethod    向具有给定名称和实现的类中添加新方法
 * @param cls         被添加方法的类
 * @param name        selector 方法名
 * @param imp         实现方法的函数指针
 * @param types imp   指向函数的返回值与参数类型
 * @return            如果添加方法成功返回 YES,否则返回 NO
 */
//BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                const char * _Nullable types);


- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
    NSLog(@"Doing foo");//新的foo函数
}

可以看到虽然没有实现foo:这个函数,但是我们通过class_addMethod动态添加fooMethod函数,并执行fooMethod这个函数的IMP。从打印结果看,成功实现了。

如果resolve方法返回 NO ,运行时就会移到下一步:forwardingTargetForSelector。

备用接收者(消息接受者重定向)

在消息转发第一次方法解析中没有处理方法,并告诉系统本对象无法处理,需另寻办法,那么系统会给予另外一个办法,就是让别的对象B来处理该问题,如果对象B能够处理该消息,那么该消息转发结束。

将未知SEL作为参数传入,寻找另外对象处理,如果可以处理,返回该对象

如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。

实现一个备用接收者的例子如下:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;//返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(foo)) {
        return [Person new];//返回Person对象,让Person对象接收这个消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end

可以看到我们通过forwardingTargetForSelector把当前ViewController的方法转发给了Person去执行了。打印结果也证明我们成功实现了转发。

完整消息转发(消息重定向)

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil ,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。
如果 -methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 -forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。

实现一个完整转发的例子如下:
// 获取函数的参数和返回值类型,返回签名

  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    // 消息重定向
  • (void)forwardInvocation:(NSInvocation *)anInvocation;
#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
    NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;//返回YES,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;//返回nil,进入下一步转发
}
//获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
    }
    
    return [super methodSignatureForSelector:aSelector];
}
 // 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;  // 从 anInvocation 中获取消息

    Person *p = [Person new];
    if([p respondsToSelector:sel]) {// 判断 Person 对象方法是否可以响应 sel
        [anInvocation invokeWithTarget:p]; // 若可以响应,则将消息转发给其他对象处理
    }
    else {
        [self doesNotRecognizeSelector:sel]; // 若仍然无法响应,则报错:找不到响应方法
    }

}

@end

可以看到,我们在 -forwardInvocation: 方法里面让 Person 对象去执行了 fun 函数。

既然 -forwardingTargetForSelector: 和 -forwardInvocation: 都可以将消息转发给其他对象处理,那么两者的区别在哪?

区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。
以上就是 Runtime 消息转发的整个流程。

我们实现了完整的转发。通过签名,Runtime生成了一个对象anInvocation,发送给了forwardInvocation,我们在forwardInvocation方法里面让Person对象去执行了foo函数。签名参数v@:怎么解释呢,这里苹果文档Type Encodings有详细的解释。

方法交换(Method-Swizzling)

  + (void)load
{
    Method test = class_getInstanceMethod(self, @selector(test));

    Method otherTest = class_getInstanceMethod(self, @selector(otherTest));

    method_exchangeImplementations(test, otherTest);
}

动态添加方法

class_addMethod(self, sel, testImp, "v@:");

void testImp (void)
{
    NSLog(@"testImp");
}

总结

调用 [receiver selector]; 后,进行的流程:

编译阶段:[receiver selector]; 方法被编译器转换为:
objc_msgSend(receiver,selector) (不带参数)
objc_msgSend(recevier,selector,org1,org2,…)(带参数)

运行时阶段: 消息接受者 recever 寻找对应的 selector。
通过 recevier 的 isa 指针 找到 recevier 的 class(类);
在 class(类) 的 method list(方法列表) 中找对应的 selector;
如果在 class(类) 中没有找到这个 selector,就继续在它的 superclass(父类)中寻找;
一旦找到对应的 selector,直接执行 recever 对应 selector 方法实现的 IMP(方法实现)。
若找不到对应的 selector,Runtime 系统进入消息转发机制。
运行时消息转发阶段:

动态解析: 通过重写 +resolveInstanceMethod: 或者 +resolveClassMethod:方法,利用 class_addMethod方法添加其他函数实现;
消息接受者重定向:如果上一步添加其他函数实现,可在当前对象中利用 -forwardingTargetForSelector: 方法将消息的接受者转发给其他对象;
消息重定向:如果上一步没有返回值为 nil,则利用 -methodSignatureForSelector:方法获取函数的参数和返回值类型。
如果 -methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 -forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
如果 -methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 -doesNotRecognizeSelector: 消息,程序也就崩溃了。

Runtime的应用

接下来我们来了解一下 Runtime 在实际开发过程中,具体的应用场景。

Method Swizzling(动态方法交换)

1. Method Swizzling(动态方法交换)简介

Method Swizzling 用于改变一个已经存在的 selector 实现。我们可以在程序运行时,通过改变 selector 所在 Class(类)的 method list(方法列表)的映射从而改变方法的调用。其实质就是交换两个方法的 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 交换方法

@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 为 原始方法的 IMP
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            // 添加失败(说明已包含原方法的 IMP),调用交换两个方法的实现
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

// 原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}

// 替换方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}

@end

注意

  1. 应该只在 +load 中执行 Method Swizzling。
    程序在启动的时候,会先加载所有的类,这时会调用每个类的 +load 方法。而且在整个程序运行周期只会调用一次(不包括外部显示调用)。所以在 +load 方法进行 Method Swizzling 再好不过了。

    而为什么不用 +initialize 方法呢。
    

    因为 +initialize 方法的调用时机是在 第一次向该类发送第一个消息的时候才会被调用。如果该类只是引用,没有调用,则不会执行 +initialize 方法。
    Method Swizzling 影响的是全局状态,+load 方法能保证在加载类的时候就进行交换,保证交换结果。而使用 +initialize 方法则不能保证这一点,有可能在使用的时候起不到交换方法的作用。

    +load 方法的调用规则为:
    先调用主类,按照编译顺序,顺序地根据继承关系由父类向子类调用;
    再调用分类,按照编译顺序,依次调用;
    +load 方法除非主动调用,否则只会调用一次。
    这样的调用规则导致了 + load 方法调用顺序并不一定确定。一个顺序可能是:父类 -> 子类 -> 父类类别 -> 子类类别,也可能是 父类 -> 子类 -> 子类类别 -> 父类类别。所以 Method Swizzling 的顺序不能保证,那么就不能保证 Method Swizzling 后方法的调用顺序是正确的。

    所以被用于 Method Swizzling 的方法必须是当前类自身的方法,如果把继承父类来的 IMP 复制到自身上面可能会存在问题。如果 + load 方法调用顺序为:父类 -> 子类 -> 父类类别 -> 子类类别,那么造成的影响就是调用子类的替换方法并不能正确调起父类分类的替换方法

  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 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现并且一些回调方法不会调用原生方法的实现这可能会造成底层实现的崩溃。

Method Swizzling 应用场景

Method Swizzling 可以交换两个方法的实现,在开发中更多的是应用于系统类库,以及第三方框架的方法替换。在官方不公开源码的情况下,我们可以借助 Runtime 的 Method Swizzling 为原有方法添加额外的功能,这使得我们可以做很多有趣的事情

全局页面统计功能

先来思考一下有几种实现方式:

  1. 手动添加
    直接在所有页面添加一次统计代码。你需要做的是写一份统计代码,然后在所有页面的 viewWillAppear: 中不停的进行复制、粘贴。

  2. 第二种:利用继承
    创建基类,所有页面都继承自基类。这样的话只需要在基类的 viewDidAppear: 中添加一次统计功能。这样修改代码还是很多,如果所有页面不是一开始继承自定义的基类,那么就需要把所有页面的继承关系修改一下,同样会造成很多重复代码,和极大的工作量。

  3. 第三种:利用分类 + Method Swizzling
    我们可以利用 Category 的特性来实现这个功能。如果一个类的分类重写了这个类的方法之后,那么该类的方法将会失效,起作用的将会是分类中重写的方法。

    这样的话,我们可以为 UIViewController 建立一个 Category,在分类中重写 viewWillAppear:,在其中添加统计代码,然后在所有的控制器中引入这个 Category。但是这样的话,所有继承自 UIViewController 类自身的 viewWillAppear: 就失效了,不会被调用。

    这就需要用 Method Swizzling 来实现了。步骤如下:

    在分类中实现一个自定义的xxx_viewWillAppear: 方法;
    利用 Method Swizzling 将 viewDidAppear: 和自定义的 xxx_viewWillAppear: 进行方法交换。
    然后在 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
字体根据屏幕尺寸适配

利用分类 + Method Swizzling

为 UIFont 建立一个 Category。
在分类中实现一个自定义的 xxx_systemFontOfSize: 方法,在其中添加缩放字体的方法。
利用 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
处理按钮重复点击

为 UIControl 或 UIButton 建立一个 Category。
在分类中添加一个 NSTimeInterval xxx_acceptEventInterval; 的属性,设定重复点击间隔
在分类中实现一个自定义的 xxx_sendAction:to:forEvent: 方法,在其中添加限定时间相应的方法。
利用 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 异常加载占位图
#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

更多
https://www.jianshu.com/p/f6dad8e1b848

Category 底层原理

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值