- SEL是方法或者函数指针吗?
- 方法签名是什么,有什么用处?
- 为什么方法转发需要先返回一个方法签名?
- 除了runtime方法外你会如何调用私有方法?
- 为什么OC没有方法重载的概念?
iOS开发中我们整日跟方法打交道,我们都知道它最后都是发送该消息,它用起来足够简单,但对于方法调用涉及到的一些知识和概念我觉得有必要再次认识一下,接下来的篇幅我将要介绍
- SEL
- IMP
- Method
- NSMethodSignature
- NSInvocation
SEL只是方法的名称
在运行时objc.h
中可以看到如下定义,SEL是一个指向objc_selector
结构体的指针,SEL可以看似是人的名字
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
在运行时源码里面我们并没有看到objc_selector
结构体的具体实现,但根据我们的打印数据我们可以认为objc_selector
内部至少包含一个c字符串的字段,可能还包含其他用来加快SEL
查找的辅助字段。
-
SEL的三种创建方式
SEL s1 = @selector(todo);
SEL s2 = NSSelectorFromString(@"todo");
SEL s3 = sel_registerName("todo");
NSLog(@"s1 %p", s1);
NSLog(@"s2 %p", s2);
NSLog(@"s3 %p", s3);
[3057:414576] s1 0x10b0ee595
[3057:414576] s2 0x10b0ee595
[3057:414576] s3 0x10b0ee595
打印输出三个SEL
变量的值,可以看到三个变量的值是一样的,这是因为SEL
是存储在静态数据区,像字符串常量一样只要是名称一样的方法他们的sel
都会是同一份内存,所以SEL
并不依赖于方法而存在,可以创建一个SEL
但是整个App可以没这个方法存在。
即使来自不同类的或者不同模块的,只要方法名称相同(比如
'setName:'
就是一个方法名称),那么这些方法的SEL
的值都一样,所有的SEL
都会由全局变量来维护;使用@selector
宏的方式创建SEL
的一个好处是在编码阶段它会在当前上下文环境中寻找对应的名称的方法,所以如果查找不到该方法会则Xcode会给出警告提示;
IMP 是方法的函数指针
IMP定义如下:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
可以看到IMP
是一个拥有多参数的函数指针,OC中的所有的方法调用最后都将转换为IMP
指针指向的函数调用
Method是SEL和IMP的一个映射关系的包装
-
Method定义
typedef struct old_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
-
方法的获取
Method class_getInstanceMethod(Class cls, SEL sel)
-
从方法列表中查找方法过程
static inline old_method *_findMethodInList(old_method_list * mlist, SEL sel) {
int i;
if (!mlist) return nil;
for (i = 0; i < mlist->method_count; i++) {
old_method *m = &mlist->method_list[i];
if (m->method_name == sel) {
return m;
}
}
return nil;
}
解析:在类的结构中包含有它的实例方法列表(Method
列表),而Method
的结构体中包含方法选择子SEL
和方法实现IMP
。 runtime
的方法查找过程,简化起来就是根据SEL
在类的方法列表中遍历查找与之匹配的SEL的方法。
这种查找过程也决定了OC语言不支持方法重载这个特性。如果支持重载,包含不止一个同名方法,那么该方法的查找只会找到第一个就直接返回,因为根据
SEL
不能确定是查找哪个重载方法;
-
至此我们能回答"为什么OC没有方法重载?"这个问题了
重载方法是【方法名称一样,但是参数个数,参数类型不一样的方法】,在这样的前提下,我们要确定一个方法就得需要方法名称SEL
和方法参数类型描述method_types
这两个条件了,上面方法查找的过程_findMethodInList
我们可以看到运行时查找方法仅仅根据方法名SEL来查找的if (m->method_name == sel)
,所以OC目前是不支持方法重载这个很多语言都有的特性的;
NSMethodSignature是对方法参数的描述
-
方法签名的本质
+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
从方法签名的生成方法可以看到方法签名只需要一个类型参数字符串就可以构造,这个类型参数是OC中通用的类型编码,字符串格式为:返回参数类型参数1类型参数2类型,比如setName(NSString *)name
的类型字符串为v@:
-
类型编码 type Encodings
runtime
中为了表达方便,对各种数据类型的表示进行了编码,所有的类型都可以用一个对应的字符来表示,其中v表示void
类型,@表示OC对象类型,:表示SEL
类型,具体可参考官方文档Type Encodings
// 类型编码枚举
enum _NSObjCValueType {
NSObjCNoType = 0,
NSObjCVoidType = 'v',
NSObjCCharType = 'c',
NSObjCShortType = 's',
NSObjCLongType = 'l',
NSObjCLonglongType = 'q',
NSObjCFloatType = 'f',
NSObjCDoubleType = 'd',
NSObjCBoolType = 'B',
NSObjCSelectorType = ':',
NSObjCObjectType = '@',
NSObjCStructType = '{',
NSObjCPointerType = '^',
NSObjCStringType = '*',
NSObjCArrayType = '[',
NSObjCUnionType = '(',
NSObjCBitfield = 'b'
}
// 练习一下类型编码
// - (void)setName:(NSString *)name;
// - (NSString *)name;
// - (void)downloadImage:(NSString *)url completionHandler:^(void(^)(UIImage *image))completionHandler;
// v@:@
// @@:
// v@:@^
// 测试代码
NSMethodSignature *methodSignature1 = [NSMethodSignature signatureWithObjCTypes:"v@:"];
NSMethodSignature *methodSignature2 = [NSMethodSignature signatureWithObjCTypes:"v@:"];
NSLog(@"methodSignature1 %p", methodSignature1);
NSLog(@"methodSignature2 %p", methodSignature2);
// 输出
// 2018-01-29 22:18:34.622 IANLearn[3057:66145] methodSignature1 0x608000266ec0
// 2018-01-29 22:18:34.622 IANLearn[3057:66145] methodSignature2 0x608000266ec0
方法签名是用来表达一个方法的参数特征,这些特征包含方法的参数个数,参数类型,返回值类型和
SEL
一样,所以只要是方法的参数特性(参数和返回值)一样,那么方法的签名就一样,所有的方法签名都会由全局变量来维护
-
NSMethodSignature查找
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector;
// 测试代码
NSMethodSignature *methodSignature3 = [self methodSignatureForSelector:@selector(application:didFinishLaunchingWithOptions:)];
NSMethodSignature *methodSignature4 = [AppDelegate instanceMethodSignatureForSelector:@selector(application:didFinishLaunchingWithOptions:)];
NSMethodSignature *methodSignature5 = [AppDelegate instanceMethodSignatureForSelector:@selector(unknowMethod)];
NSLog(@"methodSignature3 %p", methodSignature3);
NSLog(@"methodSignature4 %p", methodSignature4);
NSLog(@"methodSignature5 %p", methodSignature5);
// 输出
// 2018-01-29 22:28:11.792 IANLearn[3207:70694] methodSignature3 0x600000263900
// 2018-01-29 22:28:11.792 IANLearn[3207:70694] methodSignature4 0x600000263900
// 2018-01-29 22:28:11.793 IANLearn[3207:70694] methodSignature5 0x0
上面两个方法都是从某个类中查找指定SEL
的方法签名,整个过程大概是是去对应的类方法列表中寻找与该SEL
匹配的方法Method
,然后直接返回该Method
的签名,如果没有查找到该方法则返回nil
有一点需要说明的是在其他语言里面方法签名包含方法名称和参数信息,而OC的NSMethodSignature 仅仅包含参数信息
NSInvocation是OC的方法调用器
OC语言中有多种方式去调用方法,抛开c/c++的调用方式可以列举出如下几种:
- 对象/类对象直接调用方法
[obj method:param1:]
,这种方式是最面向对象的姿势了; -
[peformSelector withObject:]
,这种方式它可以很方便的去延迟调用,或者丢到后台线程调用,并且调用没有定义的SEL也不会导致编译错误,但它最大的问题是最多传递2个参数,没有返回值,并且参数只能是对象类型的所以不是很灵活; - block调用
block(name, age)
,闭包可以看做是匿名方法,它的调用不用去对象类中去寻找方法,本身block的结构中就包含方法的实现,它其中的方法不属于某个对象只属于闭包本身;
4.invocation
调用,invocation
调用是OC中最灵活的方法调用方式,它可以调用对象的私有方法,可以传递任意多个参数,可以兼容各种参数类型,并且可以存储方法调用的返回值。
-
invocation的构建
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
一个NSInvocation
主要由方法签名+参数值来确定, 通过方法签名创建NSInvocation
后我们给他的参数值就行,其中调用对象是第一个参数,方法名称SEL
是第二个参数
-
invocation的调用
- (void)invoke;
- (void)invokeWithTarget:(id)target;
NSInvocation
的调用有2个方法,target
参数可以直接设置target
属性或者设置为第一个参数,如果不设置则调用第二个方法将target
传入
// 测试
// 构建对象 测试UILabel的setText:方法
UILabel *myObj = [UILabel new];
NSLog(@"invocation执行前myObj.text=%@", myObj.text);
// 构建方法签名返回类型void编码为v,对象UILabel类型编码为@,SEL编码为:,参数类型NSString编码为@
NSMethodSignature *myMethodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
// 构建NSInvocation
NSInvocation *myInvocation = [NSInvocation invocationWithMethodSignature:myMethodSignature];
// 设置第1个参数
myInvocation.target = myObj;
// 设置第2个参数
myInvocation.selector = @selector(setText:);
// 设置第3个参数
NSString *newText = @"change new text";
[myInvocation setArgument:&newText atIndex:2];
[myInvocation retainArguments];
// 执行
[myInvocation invoke];
NSLog(@"invocation执行后myObj.text=%@", myObj.text);
-
invocation的使用场景
- 调用多参数的私有方法
私有方法我们不能通过对象直接调用,我们可以使用peformSelector
的方式调用,但是对于多余2个参数的情况我们就没办法调用了,这时候就可以构建NSInvocation
来调用了。 - 方法转发调用
方法转发的forwardInvocation
中需要我们去执行NSInvocation
,通常是将NSInvocation
通过更改target
参数的形式转发给其他对象来执行。
网友评论