Type Encodings 、NSMethodSignature 、NSInvocation三部曲

前言

一般来说,写东西都是由上而下顶层联想,由于这次是知识总结,准备从浅入深,一点一点写下去。文章段落之间好想关系耦合度有点低,见谅。


Type Encodings

Type Encodings作为对Runtime的补充,编译器将每个方法的返回值参数类型编码成一个C字符串,并将这个字符串和OC的selector进行关联。我们可以用编译器指令@encode来获取这个C字符串。当你给一个指定的类型(这个类型可以是基本数据类型,如int,可以是一个指针,一个结构体,类名等,也就是可以作为sizeof()参数的任何类型),@encode会返回一个将这个类型编码后的C字符串。总结为一句话:用一个C字符串来表示一个数据类型

在Objective-C Runtime Programming Guide中的Type Encoding一节中,列出了Objective-C中所有的类型编码。需要注意的是这些类型很多是与我们用于存档和分发的编码类型是相同的。但有一些不能在存档时使用。

注:Objective-C不支持long double类型。@encode(long double)返回d,与double是一样的。

下面这个列表列出了一些类型码及其代表的意义:

Code Meaning
c A char
i An int
s A short
l A long
l is treated as a 32-bit quantity on 64-bit programs.
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type...} A structure
(name=type...) A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type (among other things, this code is used for function pointers)

打印出来看看:

    NSLog(@"char --> %s",@encode(char));
    NSLog(@"int --> %s",@encode(int));
    NSLog(@"short --> %s",@encode(short));
    NSLog(@"long --> %s",@encode(long));
    NSLog(@"long long --> %s",@encode(long long));
    NSLog(@"unsigned char --> %s",@encode(unsigned char));
    NSLog(@"unsigned int --> %s",@encode(unsigned int));
    NSLog(@"unsigned short --> %s",@encode(unsigned short));
    NSLog(@"unsigned long --> %s",@encode(unsigned long long));
    NSLog(@"float --> %s",@encode(float));
    NSLog(@"bool --> %s",@encode(bool));
    NSLog(@"void --> %s",@encode(void));
    NSLog(@"char * --> %s",@encode(char *));
    NSLog(@"id --> %s",@encode(id));
    NSLog(@"Class --> %s",@encode(Class));
    NSLog(@"SEL --> %s",@encode(SEL));
    int array[] = {1,2,3};
    NSLog(@"int[] --> %s",@encode(typeof(array)));
    typedef struct person{
        char *name;
        int age;
    }Person;
    NSLog(@"struct --> %s",@encode(Person));
    
    typedef union union_type{
        char *name;
        int a;
    }Union;
    NSLog(@"union --> %s",@encode(Union));

    int a = 2;
    int *b = {&a};
    NSLog(@"int[] --> %s",@encode(typeof(b)));

打印结果:

2020-06-29 18:21:07.002557+0800 LibanayCollection[4116:440746] char --> c
2020-06-29 18:21:07.002640+0800 LibanayCollection[4116:440746] int --> i
2020-06-29 18:21:07.002702+0800 LibanayCollection[4116:440746] short --> s
2020-06-29 18:21:07.002762+0800 LibanayCollection[4116:440746] long --> q
2020-06-29 18:21:07.002828+0800 LibanayCollection[4116:440746] long long --> q
2020-06-29 18:21:07.002883+0800 LibanayCollection[4116:440746] unsigned char --> C
2020-06-29 18:21:07.002940+0800 LibanayCollection[4116:440746] unsigned int --> I
2020-06-29 18:21:07.002994+0800 LibanayCollection[4116:440746] unsigned short --> S
2020-06-29 18:21:07.003052+0800 LibanayCollection[4116:440746] unsigned long --> Q
2020-06-29 18:21:07.003115+0800 LibanayCollection[4116:440746] float --> f
2020-06-29 18:21:07.003177+0800 LibanayCollection[4116:440746] bool --> B
2020-06-29 18:21:07.003250+0800 LibanayCollection[4116:440746] void --> v
2020-06-29 18:21:07.003333+0800 LibanayCollection[4116:440746] char * --> *
2020-06-29 18:21:07.003455+0800 LibanayCollection[4116:440746] id --> @
2020-06-29 18:21:07.003600+0800 LibanayCollection[4116:440746] Class --> #
2020-06-29 18:21:07.003723+0800 LibanayCollection[4116:440746] SEL --> :
2020-06-29 18:21:07.003811+0800 LibanayCollection[4116:440746] int[] --> [3i]
2020-06-29 18:21:07.003903+0800 LibanayCollection[4116:440746] struct --> {person=*i}
2020-06-29 18:21:07.007271+0800 LibanayCollection[4116:440746] union --> (union_type=*i)
2020-06-29 18:21:07.007343+0800 LibanayCollection[4116:440746] int[] --> ^i

此时我们能想到最简单的使用,类型判断:

    NSNumber *num1 = [NSNumber numberWithInt:1];
    if (strcmp([num1 objCType], @encode(int)) == 0 ) {
        NSLog(@"--- num1 是int类型 ---");
    }
    
    NSNumber *num2 = [NSNumber numberWithFloat:1.0];
    if (strcmp([num2 objCType], @encode(float)) == 0 ) {
        NSLog(@"--- num2 是float类型 ---");
    }
    
    NSNumber *num3 = [NSNumber numberWithChar:'A'];
    if (strcmp([num3 objCType], @encode(char)) == 0 ) {
        NSLog(@"--- num3 是char类型 ---");
    }

结论:

2020-06-29 19:08:14.655418+0800 LibanayCollection[4269:463051] --- num1 是int类型 ---
2020-06-29 19:08:14.655496+0800 LibanayCollection[4269:463051] --- num2 是float类型 ---
2020-06-29 19:08:14.655569+0800 LibanayCollection[4269:463051] --- num3 是char类型 ---

当然,它的主要用处我们也提到了,将返回值参数类型编码成字符串,我们一点一点来看。

NSMethodSignature

顾名思义, NSMethodSignature就是“方法签名”,苹果官方定义该类为对方法的参数类型返回值类型进行封装 .

扒一下api:

#import <Foundation/NSObject.h>

NS_ASSUME_NONNULL_BEGIN

NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available")
@interface NSMethodSignature : NSObject

//初始化方法
+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;

//参数数量
@property (readonly) NSUInteger numberOfArguments;

//获取参数类型
- (const char *)getArgumentTypeAtIndex:(NSUInteger)idx NS_RETURNS_INNER_POINTER;

@property (readonly) NSUInteger frameLength;

//是否是单向
- (BOOL)isOneway;

//返回值类型
@property (readonly) const char *methodReturnType NS_RETURNS_INNER_POINTER;

//返回长度
@property (readonly) NSUInteger methodReturnLength;

@end

NS_ASSUME_NONNULL_END

我们可以清晰的查看到,只有一个初始化方法,+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;我们来看一下:

 NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@@:*"];

看下里边的@@:*,结合Type Encodings我们能看出第一个@是返回一个id类型。那么其它的东西是什么呢?

OC为支持消息的转发和动态调用,Objective-C Method 的 Type 信息以 “返回值 Type + 参数 Types” 的形式组合编码,还需要考虑到 self和 _cmd 这两个隐含参数:

即:@@:* 可以分解为 id(返回值) + self + _cmd + char*(参数)

当然,我们查看NSObject类的定义api,会惊喜的发现这两个几个方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

所以我们可以在类中轻松的获取签名对象:

- (void)viewDidLoad {
    [super viewDidLoad];
    //获取实例方法签名
    NSMethodSignature * signature1 = [self methodSignatureForSelector:@selector(instanceLog)];
    NSMethodSignature * signature2 = [ViewController instanceMethodSignatureForSelector:@selector(instanceLog)];
    //获取类方法签名
    NSMethodSignature * signature3 = [ViewController methodSignatureForSelector:@selector(classLog)];
}

- (void)instanceLog{
    NSLog(@"---instanceLog----");
}

+ (void)classLog{
    NSLog(@"--classLog--");
}

NSInvocation

NSInvocation是一个消息调用类,主要作用是存储和传递消息。它存储的信息包含了一个iOS消息全部的成分:target、selector、参数、返回值、方法签名。也就是说,NSInvocation可以将传统的iOS消息发送这个过程转换成一个对象,然后执行这个对象的发送消息的方法就可以发送消息,NSInvocation对象包含的每一个组成部分能够直接设定(如消息target,参数之类的)。

先上api:

@interface NSInvocation : NSObject
//初始化方法
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
//方法签名
@property (readonly, retain) NSMethodSignature *methodSignature;
//防止参数释放掉
- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;
//target
@property (nullable, assign) id target;
//SEL
@property SEL selector;
//获取返回值
- (void)getReturnValue:(void *)retLoc;
//设置返回值
- (void)setReturnValue:(void *)retLoc;
//获取参数
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
//设置参数
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
//执行
- (void)invoke;
// target发送消息,即target执行方法
- (void)invokeWithTarget:(id)target;
@end

初始化方法需要传递一个NSMethodSignature对象,我们在上文中已经掌握了 NSMethodSignature,就不多做解释了,那么我们如何使用呢,我们开始修改上边的代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // -- 获取instanceLog的签名
    NSMethodSignature * signature = [self methodSignatureForSelector:@selector(instanceLog)];
    // -- 根据方法签名创建一个NSInvocation
    NSInvocation * invocation = [NSInvocation  invocationWithMethodSignature:signature];
    // -- 设置调用者
    [invocation setTarget:self];
    // -- 设在被调用的消息
    [invocation setSelector:@selector(instanceLog)];
    // --  执行
    [invocation invoke];
}

- (void)instanceLog{
    NSLog(@"---instanceLog----");
}

执行结果:

2020-06-30 15:48:55.647225+0800 LibanayCollection[6359:869723] ---instanceLog----

结论:
NSInvocation可以看做命令模式在iOS中的实现,即将目标(target)、选择器(selector)、方法签名(NSMethodSignature)、参数(argument)等信息打包在一起使用。

用于消息转发

最初对于NSInvocation和NSMethodSignature的认知,我是从消息转发流程开始的,不知道大家是否是一样的。不太了解的可以看我另一篇文章 iOS消息机制消息转发详解

与performSelector:的联系与区别

iOS中可以直接调用某个对象的消息方式有两种:一种是performSelector:withObject;另一种就是NSInvocation, 关于performSelector能完成简单调用,我们无需多言。但是对于>2个的参数或者有返回值的处理,那performSelector:withObject就显得有点有心无力了,那么在这种情况下,我们就可以使用NSInvocation来进行这些相对复杂的操作。我们继续修改上边代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // -- 获取instanceLog的签名
    NSMethodSignature * signature = [self methodSignatureForSelector:@selector(instanceLogStr1:andStr2:)];
    // -- 根据方法签名创建一个NSInvocation
    NSInvocation * invocation = [NSInvocation  invocationWithMethodSignature:signature];
    // -- 设置调用者
    [invocation setTarget:self];
    // -- 设在被调用的消息
    [invocation setSelector:@selector(instanceLogStr1:andStr2:)];
    NSString * str1 = @"test1";
    NSString * str2 = @"test2";
    // -- 添加参数
    [invocation setArgument:&str1 atIndex:2];
    [invocation setArgument:&str2 atIndex:3];
    // -- 执行
    [invocation invoke];
}

- (void)instanceLogStr1:(NSString *)str1 andStr2:(NSString *)str2{
    NSLog(@"---instanceLog || %@ || %@----",str1,str2);
}

执行结果:

2020-06-30 16:32:27.277291+0800 LibanayCollection[6488:890762] ---instanceLog || test1 || test2----

这里要注意点是添加参数从Index2开始,这是因为还需要考虑到 self和 _cmd 这两个隐含参数。

结语:

这篇写的大部分都是概念性的东西,也找了很多文章来学习进行归纳。写好一篇文章不容易,本文最下方的参考文献希望各位看官有机会也去读一下,互相印证。

文献参考

OC-类型编码(TypeEncodings)
iOS 如何实现Aspect Oriented Programming
NSMethodSignature与NSInvocation使用
NSInvocation NSMethodSignature 全面解析
NSInvocation的基本用法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354