iOS-有效编写高质量Objective-C方法四

看到上一次发这个系列的文章,已经是一个月之前了,我的妈呀,这个时间过得挺快的,一下一个来月过去了,到现在才来更,真是对不起大家了,当时我记得是说重新来读一遍Effective Objective-C 确实呢,书有在读,就完成了第二章节,现在呢,我们继续。。。

理解objc_msgSend的作用

在对象上调用方法是Objective-C常用的功能。用Objc的术语来讲,叫做“传递消息”。消息有“名称(name)”或“选择子(selector)”,可以接受参数,而且可以有返回值。

由于OC是C的超集,所以最好先理解C语言的函数调用方式。C语言使用“静态绑定”,也就是说,在编译期间,就能够知道所应该调用的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

void printHello(){
printf("Hello world\n");
}

void printGoodBye(){
printf("Goodbye world\n");
}

void doTheThing(int type){
if (type == 0) {
printHello();
}else{
printGoodBye();
}
}

如果不考虑内联关系,那么编译器在编译代码的时候就已经知道了程序中有printHello和printGoodBye这两个函数了,如果我们将编写的方式改变一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void printHello(){
printf("Hello world\n");
}

void printGoodBye(){
printf("Goodbye world\n");
}

void doTheThing(int type){
void (*fnc)();
if (type == 0) {
fnc = printHello;
}else{
fnc = printGoodBye;
}
fnc();
}

这下,就必须要使用动态绑定了,因为需要调用的函数,在运行期间才能够知道。在objc中,要向对象传递消息,就必须要使用到动态绑定的机制来决定需要调用的方法。

给对象传递消息,可以这样写

1
id returnValue = [someObject messageName:parameter];

在上面代码中,someObject叫做接收者,messageName叫做选择子,选择子和参数合起来叫做消息。编译器在看到该消息后,将其转化成为一条标准的C语言函数调用,所调用的函数叫做objc_msgSend,其原型:

1
void objc_msgSend(id self,SEL cmd,...)

这是一个参数可变的函数,其中第一个参数代表的是接收者,第二个代表的是选择子,第三个以及后面的代表的是参数,编译器会把上面的传递消息的例子改为:

1
2
id returnValue = objc_msgSend(someObject,@selector(messageName:),
parameter);

objc_msgSend函数会根据接收者和选择子来选择调用的方法,改函数会在接收者的类中寻找方法列表,如果能找到与选择子名称相符的方法,就跳至其,实现代码,如果找不到,那就继续沿着继承体系往上找,如果最终还是找不到方法,那么就会执行”消息转发“操作

这样看来,好像我们调用一个方法需要很多步骤,所幸的是,objc_msgSend会将匹配结果缓存子啊一张快速映射表中,这样一来,每个类都会有一个缓存,这样虽然第一次执行起来会稍慢,但是后面就会很迅速了。

理解消息转发机制

上面我们讲了消息传递机制,接下来我们继续讲另外一个重要的问题,就是对象在接收到无法解读的消息时,会发生什么?

如果我们想让类理解某条消息,我们必须以程序代码的形式实现出对应的方法才行,但是在编译期间,向类发送了无法解读的方法并不会报错,因为在运行期间可以继续向类中添加方法,所以编译器在编译时还无法确定类中是否有某个方法的实现。当对象接收到无法解读的方法时,就会启动消息转发机制,我们可以经此告诉对象应该如何处置这条消息。

我们之前在编码过程中肯定有遇到过下面这种错误:

1
2
3
-[__NSCFNumber lowercaseString] : unrecognized selector sen to instance 0x87
...
...

上面这一段消息是由NSObject的 “doesNotRecognizeSelector:” 方法跑出的,此异常表明:消息接收者的类型是 __NSCFNumber,而该接收者无法理解名为lowercaseString的选择子。因为NSNumver中本就没有这个方法。

消息转发分为两大阶段:

  • 第一阶段 先征询接收者,所属的类,看其是否能动态添加方法,用以处理当前这个“未知的选择子”,这叫做“动态方法解析”。
  • 第二阶段 涉及到了完整的消息转发机制,如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法来响应包含选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这里又可分为两步:
    首先,请接收者看看有没有其他的对象能够处理这条消息,若有,运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。若没有,备援的接收者则会启动完整的消息转发机制,运行期系统会把与消息相关的所有细节全部封装到NSInvocation对象中,再给接收者一次机会,令其处理。
动态方法解析

在对象收到无法解析的消息后,首先会调用其所属类的下列方法

1
+ (Bool)resolveInstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,返回的值表示该类是否能够新增一个方法来处理当前的选择子

假设未实现的方法不是实例方法,而是类方法,那么运行期系统会调用另外一个方法 叫做 resolveClassMethod:

使用动态方法解析的前提是:相关的代码已经写好了,只等运行的时候动态插入到类中即可。
这种方案常用来实现@dynamic属性,这个属性之前我们已经讲过,有疑问的同学翻看一下我前面的博客。

比如说我们要访问CoreData框架中的NSManagerObjects对象的属性时,就可以这样做,因为这些属性所需要的存取方法在编译器就可以确定。

我们模拟一下,使用resolveInstanceMethod方法,来实现@dynamic属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);

+ (Bool)resolveInstanceMethod:(SEL)selector{
NSString *selectorString = NSStringFromSelector(selector);
if(/* selector is from a @dynamic property */){//这里的意思是判断属性是声明成了@dynamic
if([selectorString hasPrefix:@"set"]){
class_addMethod(self,
selector,
(IMP)autoDictionarySetter),
"v@:@");
}else{
class_addMethod(self,
selector,
(IMP) autoDictionaryGetter),
"@:@");
}
return YES;
}
return [super resolveInstanceMethod:selector];
}

这样,我们已经在代码中处理了 set方法和get方法

备援接收者

当前的接收者除了resolveInstanceMethod,还有第二次机会处理未知的选择子。在这一步中,运行系统会询问,能否将这一消息转发给其他的接收者来处理

1
- (id)forwardingTargetSelector:(SEL)selector;

如果找到当前的备援者,则将其直接返回,如果找不到,则返回Nil

为什么会存在备援者呢?在一个对象内部,可能还有其他一系列对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,就像是自己处理的一样。

完整的消息转发

如果备援者还找不到,那么就需要执行完整的消息转发机制。首先创建NSInvocation对象,将尚未处理的消息相关的所有细节,全部封装。在触发NSInvocation对象时,”消息派发系统”将亲自将消息指派给目标对象

1
- (void)forwardInvocation:(NSInvocation *)invocation;

这个方法很简单,只需要改变调用目标,是消息在新目标上调用,但是这样就和备援者差不多了,我们一般这样处理:
在触发消息之前,先以某种方式改变消息的内容,比如改参数或者改选择子

实现此方法时,若发现某调用操作不应该由本类实现,那么就由其超类同名方法实现,直至NSObject

消息转发全过程