在
Objective-C
中使用发送消息的形式来调用方法,其中涉及到
Runtime
库中定义的
SEL
、
IMP
、
Method
,它们分别表示什么,以及它们之间的关系。
一、SEL(selector)
selector
其实只是一个SEL
类型的一个实例:
@property SEL selector;
SEL
:一个不透明的类型,代表方法的选择器/选择子。定义如下:
typedef struct objc_selector *SEL;
// 是一个指向 objc_selector 结构体的指针
// objc_selector 未开源
在源码中没有直接找到objc_selector
的定义,从一些书籍上与Blog
上看到可以将SEL
理解为一个char*
指针。
// GNU OC 中的 objc_selector
struct objc_selector {
void *sel_id;
const char *sel_types;
};
在运行时,方法选择器用来表示方法的名字。一个方法选择器就是一个C字符串,在OC的运行时被注册。编译器生成选择器在类加载时由运行时自动映射。
您可以在运行时添加新的选择器,并使用sel_registerName
函数检索现有的选择器。
必须从sel_registerName
或@selector()
(OC编译器指令)返回的值,获得选择器(用NSSelectorFromString
也可以)。不能简单地将C字符串转换为SEL
。
测试:
// 获得SEL的三种方式
SEL selA = @selector(setTitle:);
SEL selB = sel_registerName("setTitle:");
SEL selC = NSSelectorFromString(@"setTitle:");
// 输出
(lldb) po selA
"setTitle:"
(lldb) po selB
"setTitle:"
(lldb) po selC
"setTitle:"
还可以使用sel_getName
将SEL
转回成Char*
:
NSLog(@"%s", sel_getName(selB));
// setTitle:
从sel_getName
的源码可以看出SEL
和const char *
是可以相互转化的:
const char *sel_getName(SEL sel) {
if (!sel) return "<null selector>";
return (const char *)(const void*)sel;
}
注意:
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同,也会导致它们具有相同的方法选择子。比如:
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;
这样定义会报错,因为这两个方法的选择器是一样的,因此OC中不允许方法重载(因为只能靠方法名来标识方法)。
触发 SEL,并消除 warning:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
BOOL hasIvar = [self performSelector:hasIvarSel];
#pragma clang diagnostic pop
二、IMP(implementation)
指向方法实现的首地址
的指针。源码里实现如下:(可以看得出来是对方法类型
起了一个别名。)
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
IMP
的数据类型是指针,指向方法实现开始的位置。这样的方法使用标准的C调用协议
(此协议是为当前CPU体系结构实现的)。
- 第一个参数是指向
self
的指针(实例的内存<实例方法> 或 元类的指针<类方法>); - 第二个参数是方法的选择器;
- 接下来是方法的参数。
一些使用:
// 返回方法的具体实现
IMP class_getMethodImplementation(Class cls, SEL name);
IMP class_getMethodImplementation_stret(Class cls, SEL name);
// 类实例是否响应指定的selector
BOOL class_respondsToSelector(Class cls, SEL sel);
IMP的高级作用
既然上述元素都确定下来了,那么就可以直接绕过Runtime
的消息传递机制,直接执行IMP
指向的函数了。省去了一些列的查找,直接向对象发送消息,效率会高一些。
// 根据代码块获取IMP, 其实就是代码块与IMP关联
IMP imp_implementationWithBlock(id block)
// 根据Method获取IMP
IMP method_getImplementation(Method m)
// 根据SEL获取IMP
[[objc Class] instanceMethodForSelector:SEL]
当我们获取一个方法的IMP
后,可以直接调用IMP
:
IMP imp = method_getImplementation(Method m);
// result保存方法的返回值,id表示调用这个方法的对象,SEL是Method的选择器,argument是方法的参数。
id result = imp(id, SEL, argument);
三、Method
一个不透明的类型,表示类中定义的方法。定义如下:
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
从代码的定义可以看出,Method
是一个指向objc_method
结构体类型的指针。objc_method
结构体中定一个三个属性:
-
method_name
:SEL
类型(选择器),表示方法名的字符串。 -
method_types
:char*
类型的,表示方法的类型;包含返回值和参数的类型。 -
method_imp
:IMP
类型,指向方法实现地址的指针。
Method操作函数如下:
方法操作主要有以下函数:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types); // 添加方法
Method class_getInstanceMethod(Class cls, SEL name); // 获取实例方法
Method class_getClassMethod(Class cls, SEL name); // 获取类方法
Method *class_copyMethodList(Class cls, unsigned int *outCount); // 获取所有方法的数组
// 替代方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
// 交换两个方法的实现
method_exchangeImplementations(Method m1, Method m2)
总结
SEL
:就是一个字符串(Char*
类型),表示方法的名字
IMP
:就是指向方法实现首地址的指针
Method
:是一个结构体,包含一个SEL
表示方法名、一个IMP
指向函数的实现地址、一个Char*
表示函数的类型(包括返回值和参数类型)
-
SEL
、IMP
、Method
之间的关系可以这么理解:一个类(
Class
)持有一系列的方法(Method
),在load
类时,runtime
会将所有方法的选择器(SEL
)hash
后映射到一个集合(NSSet
)中(NSSet
里的元素不能重复)。
当需要发消息时,会根据选择器(SEL
)去查找方法;找到之后,用Method
结构体里的函数指针(IMP
)去调用方法。这样在运行时查找selecter
的速度就会非常快。
参考: