欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

理解 id, Class, isa, SEL, IMP, Method

程序员文章站 2022-04-12 23:21:22
...

理解 id, Class, isa, SEL, IMP, Method

id 类型

首先介绍两个概念:静态类型和动态类型。

静态类型:将一个指针变量定义为特定类的对象时,使用的是静态类型,在编译的时候就知道这个指针变量所属的类,比如:(下面的p指针就是静态类型)

NSString *p = [[NSString alloc] init];

动态类型:程序运行时才确定变量的类,比如:(下面的p指针在编译的时候不知道自己的类型,直到运行时才直到)

id p = [[NSString alloc] init];

id就是一种动态类型,它可以指向属于任何继承与NSObject类的对象,这样id就可以调用项目中所有继承与NSObject类的方法(但可能调用的方法不在对象中,要小心使用),比如:

@interface Person : NSObject
- (void)speak;
@end
  
@implementation Person
- (void)speak {
    NSLog(@"hello");
}
@end

id p = [[NSString alloc] init];
[p speak];  // 编译可以通过,运行时报错:unrecognized selector sent to instance 0x7fff8b02f288"

id在内部是如何实现的呢?其实id 是一个结构体指针类型:

typedef struct objc_object *id {
    Class isa;
};

可以看出id内部有一个Class类型的指针isa,这里引入了两个新概念:Classisa

Class 类型和 isa 指针

Class 也是一个结构体指针类型:

typedef struct objc_class *Class {
  	Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    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
};

id是对象的类型,Class就是类的类型了,id通过isa指针,指向了他的类,他的类的类型就是Class

Class结构体的定义中,我们可以看到,内部有一个isa指针,指向类的元类(meta class),有一个super_class指针,指向Class的父类,还有objc_ivar_listobjc_method_listobjc_protocol_list结构体指针,分别指向类的实例的成员变量列表、方法列表和协议列表。

因为元类和父类也都是 Class类型,我们可以推测出元类和父类也有和类相同的数据结构(有元类和父类、成员变量列表、方法列表和协议列表等),而且类的方法和类的变量都储存在元类中,子类通过继承的方式可以访问父类的方法列表和父类的元类的方法类别。但元类和父类不可能无限循环的指向更高一级的元类和父类,OC中类的关系到底是怎样的呢?下面这张经典的关系图说明了一切:

理解 id, Class, isa, SEL, IMP, Method

其中Root class就是NSObject,所有类的元类的isa指针都指向了NSObject的元类,NSObject的元类的isa指针指向了自己,结束了循环。NSObject的元类的父类指向了NSObjectNSObject的父类为空。

SEL 类型

SEL是一个结构体指针类型,也就是selector:

typedef struct objc_selector *SEL;

然而官方并没有给出objc_selector这个结构体的定义,下面总结已知的所有信息:

selector是指向objc_selector结构体的指针, selector 是一个已被Objective-C运行时注册过或映射过的C语言字符串, 如果想创建 selector ,必须使用 sel_registername 的返回值或者编译器指令@selector()(官方解释)。

这里说selector是C语言字符串,我们打印试试:

SEL testSelector = @selector(init);
NSLog(@"%s", testSelector);  // init

结果还真是,可以推测,objc_selector结构体内部的第一个成员很有可能就是一个 char * 类型的字符串,保存着 selector 的名字(因为结构体指针指向的地址其实就是第一个成员的地址,这与数组指针指向的地址和首元素地址相同是一样的道理)。其他信息就无从得知了,至少目前苹果没有公布相关的代码,可能是出于安全的考虑吧。

sel_registername 的返回值是创建selector的唯一方法,虽然编译器指令 @selector() 找不到具体实现,其底层底层仍然是使用的函数 sel_registername,验证如下:

编译main.m中的如下代码(clang -rewrite-objc main.m):

SEL testSelector = @selector("init");

结果如下:

 SEL testSelector = sel_registerName("init");

sel_registerName是怎么实现的呢?简单的总结就是:

  1. 如果方法名为空,返回0。
  2. 如果方法名内建函数同名,返回内建函数的 selector。
  3. 如果方法名如方法名不与内建函数同名,则以传入的方法名为键,在NXMapTable(一个哈希表,键为方法名,值为方法名对应的selector)中匹配,如果NXMapTable中有这个方法名 ,返回这个方法名的selector。
  4. 如果方法名未在NXMapTable中找到,那么会新创建一个selector,并与方法名互相映射成键值对,保存在NXMapTable中。

调用SEL

SEL testSelector1 = @selector(alloc);
SEL testSelector2 = @selector(init);
NSString *testString = [[NSString performSelector:testSelector1] performSelector:testSelector2];

获取SEL的方法名:

SEL testSelector1 = @selector(alloc);
NSString *testString = NSStringFromSelector(testSelector1);
NSLog(@"%@", testString);  // alloc

IMP

IMP(implement)实际上是一个函数指针,指向方法实现的首地址。代表了方法的最终实现。其定义如下:

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

对一个参数是id类型,是一个指向消息接收对象的指针,如果是实例方法,则是类实例的内存地址;如果是类方法,则是类对象的指针。第二个参数是方法选择器(selector),省略号是方法的参数。返回的是id,我们可以推测:方法的返回类型可以是任何对象类型(也可以是nil),具体在运行的时候动态决定。

这样,通过对象、方法选择器和方法参数,就可以确定要调用的方法,调用IMP的就如同调用一个函数:

@interface Person : NSObject
- (NSString *)speak;
@end
  
@implementation Person
- (NSString *)speak {
    return @"hello";
}
@end

Person *person = [[Person alloc] init];
SEL testSelector = @selector(speak);
IMP imp = [person methodForSelector:testSelector];
// 调用 imp
NSLog(@"%@", imp(person, testSelector));  // hello
// 等价于:
NSLog(@"%@", [person performSelector:testSelector]);  // hello

Method

Method的定义如下:

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

可知,Method类型是一个结构体指针,包含了SELIMP,可知一个Method类中包含了一个方法选择器SEL,这个方法选择器的调用函数IMP,也就是说,给我们一个类和一个方法选择器,那我们就可以确定一个Method。那具体有什么用呢?可以用在方法交换上(Method Swizzling)

比如交换实例方法:

@interface Person : NSObject
- (void)speak;
- (void)run;
@end

@implementation Person
- (void)speak {
    NSLog(@"hello");
}
- (void)run {
    NSLog(@"run");
}
@end
  
Person *person = [[Person alloc] init];
Method m1 = class_getInstanceMethod([Person self], @selector(speak));
Method m2 = class_getInstanceMethod([Person self], @selector(run));
method_exchangeImplementations(m1, m2);
[person speak];  // run
[person run];  // speak

交换类方法:

@interface Person : NSObject
- (void)speak;
- (void)run;
@end

@implementation Person
+ (void)speak {
    NSLog(@"hello");
}
+ (void)run {
    NSLog(@"run");
}
@end

Method m1 = class_getClassMethod([Person self], @selector(speak));
Method m2 = class_getClassMethod([Person self], @selector(run));
method_exchangeImplementations(m1, m2);
[Person speak];
[Person run];

Method Swizzling的实现机理,就是交换了MethodIMP的指针的指向,来交换方法的具体实现。