Runtime系列4之-方法

前面我们讨论了Runtime中对类和对象的处理,及对成员变量与属性的处理.这一章,我们就要开始讨论方法操作函数。

方法

  1. 主要讲专门针对方法的一些数据结构以及针对这些数据结构的操作函数.
  2. 针对方法的数据结构主要有3个:
    1. SEL
    2. IMP
    3. Method
  3. 下面我们来分别分析:

SEL

  1. SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:

     typedef struct objc_selector *SEL;
    
  2. objc_selector结构体的详细定义没有在<objc/runtime.h>头文件中找到。方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。
  3. 两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。如在某个类中定义以下两个方法:
    1. 这样的定义被认为是一种编译错误

       - (void)setWidth:(int)width;
       - (void)setWidth:(double)width;
      
  4. 当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
  5. 工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!!
  6. 本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。
  7. 我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:
    1. sel_registerName函数
    2. Objective-C编译器提供的@selector()
    3. NSSelectorFromString()方法

IMP

  1. IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:

     id (*IMP)(id, SEL, ...)
    
    1. 这个函数使用当前CPU架构实现的标准的C调用约定。
    2. 第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
    3. 第二个参数是方法选择器(selector),接下来是方法的实际参数列表。
  2. 前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP
  3. 取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。
  4. 通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。
  5. C语言的函数名就是IMP,因为函数名就是函数的地址

     //定义函数test
     void test(){
     }
     //获得IM
     IM im = (IM)test;
    

Method

  1. Method用于表示类定义中的方法,则定义如下:

     typedef struct objc_method *Method;
     struct objc_method {
         SEL method_name OBJC2_UNAVAILABLE; // 方法名
         char *method_types OBJC2_UNAVAILABLE;
         IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
     }
    
  2. 我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。具体操作流程我们将在下面讨论。

objc_method_description

objc_method_description定义了一个Objective-C方法,其定义如下:

struct objc_method_description { SEL name; char *types; };

如何获取Method

获取Method的方法只有3个,他们都是类操作方法,因为方法列表放在类或者元类中:

// 获取实例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 获取类方法
Method class_getClassMethod ( Class cls, SEL name );
// 获取所有方法的数组
Method * class_copyMethodList ( Class cls, unsigned int *outCount );

方法相关操作函数

Runtime提供了一系列的方法来处理与方法相关的操作,无非就是针对上面3中数据的获取与调用,如下:

方法(Method)

  1. 从Method指向的结构体我们不难猜出,这些操作方法大部分就是针对真个结构体的成员变量,方法操作相关函数包括以下:

     // 调用指定方法的实现
     id method_invoke ( id receiver, Method m, ... );
     // 调用返回一个数据结构的方法的实现
     void method_invoke_stret ( id receiver, Method m, ... );
     // 获取方法名
     SEL method_getName ( Method m );
     // 返回方法的实现
     IMP method_getImplementation ( Method m );
     // 获取描述方法参数和返回值类型的字符串
     const char * method_getTypeEncoding ( Method m );
     // 获取方法的返回值类型的字符串
     char * method_copyReturnType ( Method m );
     // 获取方法的指定位置参数的类型字符串
     char * method_copyArgumentType ( Method m, unsigned int index );
     // 通过引用返回方法的返回值类型字符串
     void method_getReturnType ( Method m, char *dst, size_t dst_len );
     // 返回方法的参数的个数
     unsigned int method_getNumberOfArguments ( Method m );
     // 通过引用返回方法指定位置参数的类型字符串
     void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
     // 返回指定方法的方法描述结构体
     struct objc_method_description * method_getDescription ( Method m );
     // 设置方法的实现
     IMP method_setImplementation ( Method m, IMP imp );
     // 交换两个方法的实现
     void method_exchangeImplementations ( Method m1, Method m2 );
    
    1. method_invoke函数:
      1. 返回的是实际实现的返回值。
      2. 参数receiver不能为空。
      3. 这个方法的效率会比method_getImplementationmethod_getName更快。
    2. method_getName函数:
      1. 返回的是一个SEL。
      2. 如果想获取方法名的C字符串,可以使用sel_getName(method_getName(method))
      3. 如果想获取方法名的OC字符串,可以使用NSStringFromSelector(method_getName(method))
    3. method_getReturnType函数:
      1. 类型字符串会被拷贝到dst中。
    4. method_setImplementation函数:
      1. 注意该函数返回值是方法之前的实现。

方法选择器

选择器相关的操作函数包括:

// 返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
SEL sel_registerName ( const char *str );
// 在Objective-C Runtime系统中注册一个方法
SEL sel_getUid ( const char *str );
// 比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
  1. sel_registerName函数:
    1. 在我们将一个方法添加到类定义时,我们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器。

举例实现:

例1:Runtime交换方法实现

  1. 情况: 当系统自带的方法功能不够,需要给系统自带的方法扩展一些功能,并且保持原有的功能时,可以使用RunTime交换方法实现。
  2. 需求: 这里要实现image添加图片的时候,自动判断image是否为空,如果为空则提醒图片不存在。

  3. 实现该功能的方法有两种:
    1. 通过分类直接添加方法
    2. RunTime交换方法

方法1: 代码如下:

//UIImage+RuntimeAdd.h文件
#import <UIKit/UIKit.h>

@interface UIImage (RuntimeAdd)
+ (nullable UIImage *)xx_ccimageNamed:(NSString *_Nullable)name;
@end

//UIImage+RuntimeAdd.m文件
#import "UIImage+RuntimeAdd.h"
#import <objc/runtime.h>
@implementation UIImage (RuntimeAdd)

+ (nullable UIImage *)xx_ccimageNamed:(NSString *)name
{
    // 加载图片    如果图片不存在则提醒或发出异常
    UIImage *image = [UIImage imageNamed:name];
    if (image == nil) {
        NSLog(@"图片不存在");
    }
    return image;
}
@end
//Viewcontroller中使用
#import "ViewController.h"
//必须导入
#import "UIImage+RuntimeAdd.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //调用分类中的方法
    [UIImage xx_ccimageNamed:@"kl;jkl;"];
}

缺点: 每次使用都需要导入头文件,并且如果项目比较大,之前使用的方法全部需要更改。

方法2: 代码如下:

#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@implementation UIImage (RuntimeAdd)
+(void)load{
    //1. 首先得获取要交换的两个方法实现
    // 获取类方法  用Method 接收一下
    // class :获取哪个类方法
    // SEL :获取方法编号,根据SEL就能去对应的类找方法。
    //1.1 获取系统方法实现
    Method oriMethod = class_getClassMethod([self class], @selector(imageNamed:));
    //1.2 获取自定义方法实现
    Method customMethod =class_getClassMethod([self class], @selector(zh_imageNamed:));
    //2. 交换方法的实现
    method_exchangeImplementations(oriMethod, customMethod);
}

+(UIImage *)zh_imageNamed:(NSString *)name{
    // 加载图片:如果图片不存在则提醒或发出异常
    NSLog(@"是死循环?");
    //这里其实调用的是UIImage的imageNamed:方法
    UIImage *image = [UIImage zh_imageNamed:name];
    //如果再调用这个就回引发死循环了(调用系统的方法就回调用zh_imageNamed方法)
    //    UIImage *image = [UIImage imageNamed:name];
    if (image == nil) {
        NSLog(@"图片不存在");
    }
    return image;
}
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
//不用导入分类头文件,用户仍然调用原来的方法
    [UIImage imageNamed:@"xxxxx"];
}

分析:

  1. 将UIImage+RuntimeAdd.m拖入项目即可,不需要其他任何操作
  2. 我们创建的分类不需要.h文件,因为我们并没有给原来的类UIImage添加新的方法.我们只是重写了原来类的+load方法.因此也不需要导入头文件
  3. 仍然使用原来的方法,跟之前没有任何区别
  4. 交换方法的本质其实是交换两个方法的实现,即调换xx_ccimageNamedimageName方法,达到调用xx_ccimageNamed其实就是调用imageNamed方法的目的
  5. 那么首先需要明白方法在哪里交换,因为交换只需要进行一次,所以在分类的load方法中,当加载分类的时候交换方法即可。
  6. 交换方法内部实现:
    1. 根据SEL方法编号在Method中找到方法,两个方法都找到
    2. 交换方法的实现,指针交叉指向。如图所示:
    3. 注意:交换方法时候 xx_ccimageNamed方法中就不能再调用imageNamed方法了,因为调用imageNamed方法实质上相当于调用 xx_ccimageNamed方法,会循环引用造成死循环。
    4. 此时,当调用imageNamed:方法的时候就会调用xx_ccimageNamed:方法,为image添加图片,并判断图片是否存在,如果不存在则提醒图片不存在。
  7. 过程: 此时的逻辑是,当我们在外面使用 imageNamed:为图片赋值,会来到分类的xx_ccimageNamed:方法,在xx_ccimageNamed:分类方法中会先为图片赋值,然后检查图片是否存在。而为图片赋值本来应该使用系统的imageNamed方法,但是因为交换方法了,所以此时需要使用xx_ccimageNamed: 方法。

例2:Runtime方法替换

  1. 情况:在程序当中,假设Person的中有test1这个方法,但是由于某种原因,我们要改变这个方法的实现,但是又不能去动它的源代码(正如一些开源库出现问题的时候),这个时候runtime就派上用场了。
  2. 解决:我们先增加一个Tool类,然后写一个我们自己实现的方法-change,通过runtime把test1替换成change。
  3. 主要用到这个方法:
    class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

代码如下:

//被替换类
#import <Foundation/Foundation.h>

@interface OrignTool : NSObject
-(void)test1;
@end

#import "OrignTool.h"

@implementation OrignTool
-(void)test1{
    NSLog(@"我是OrignTool的test1原始方法");
}
@end

//替换类
#import <Foundation/Foundation.h>

@interface CustomTool : NSObject
-(void)customTest;
@end

#import "CustomTool.h"
#import "OrignTool.h"
#import <objc/runtime.h>
@implementation CustomTool
//我要用我的方法替换掉OrignTool的方法,那么肯定是不能在OrignTool方法中实现替换工作了
+(void)load{
    //1. 首先得获取要交换的两个方法实现
    // 获取实例方法  用Method 接收一下
    // class :获取哪个类方法
    // SEL :获取方法编号,根据SEL就能去对应的类找方法。
    //1.1 获取OrignTool方法实现
    Method oriMethod = class_getInstanceMethod([OrignTool class], @selector(test1));
    //1.2 获取自定义方法实现
    Method customMethod =class_getInstanceMethod([self class], @selector(customTest));
    //2. 替换方法的实现
    /*
     cls: 要替换的类
     name: 要替换类的哪个方法
     imp : 替换的新的方法的实现
     types: 替换新方法的类型编码
     替换之后,@selector(test1) 对应的IMP可是customTest了
     */
    class_replaceMethod([OrignTool class], @selector(test1), method_getImplementation(customMethod), method_getTypeEncoding(customMethod));
    
    //2. 或者直接交换方法实现
    //OrignTool类中的test1方法,与当前类中的customTest方法进行交换(目的就是将两个方法的指针交叉指向)
//    method_exchangeImplementations(oriMethod, customMethod);
    //区别: 方法替换,被替换的类的方法会改变,当前类的方法不会改变
    //方法交换: 被替换类与当前类的方法都会改变,互相交叉
}
-(void)customTest{
    NSLog(@"我是CustomTool的customTest方法");
}
@end

#import "ViewController.h"
#import "OrignTool.h"
#import "CustomTool.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [[OrignTool new] test1];
    [[CustomTool new] customTest];
}

注意:

  1. 方法交换与方法替换都可以在不同类之间进行
  2. 即不一定交换同一个类的方法,也可以是两个类的方法进行交换(OrignTool与CustomTool)
  3. 可以用自己类中的方法去替换自己类中的另外一个方法,也可以用其他类的方法去替换自己类中的方法
Table of Contents