理解OC内部的消息调用、消息转发、类和对象(一)

之前看过Effective Objective-C 2.0 这本书的同学可能会注意到,我之前写的关于这本书的笔记,跳过了其中的几个章节,那时候没有记下来,是觉得我自己还没有理解到位,还不能贸然瞎说,最近仔细研究了一下,还看了Runtime和Runloop,渐渐的开始理解这些东西了,现在慢慢开始记录一下,大家一起学习。

对象的方法调用objc_msgSend

之前已经讲过了消息派发,我决定呢将这个放在这篇文章里来,这样一起看整个一个OC的运行期进行的一些操作,理解起来也会有益处的。

在对象实例上调用方法是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会将匹配结果缓存子啊一张快速映射表中,我们在之后会讲到这个缓存。这样一来,每个类都会有一个缓存,这样虽然第一次执行起来会稍慢,但是后面就会很迅速了。

类和对象

要讲消息转发机制,首先我们需要先了了解一下OC中的类和对象,OC的runtime是开源的,我们可以先从这里下载到源码,对着源码来看

https://opensource.apple.com/tarballs/objc4/

首先,我们来看一下类和对象的定义,在objc.h文件和runtime.h文件中分别定义了对象和类

1
2
3
4
5
6
7
8
9
10
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

从上面的代码中,我们可以看出,在OC中id代表的是一个OC对象,Class代表的是OC中的一个类。在对象定义中,首地址是一个*isa的struct的指针。这个指针指向对象所代表的类(Class)。

我们再来看看类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct objc_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

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

我们先抛开其他的不看,一眼就看到了类的首地址也是一个*isa的指针,这个就说明,其实类他也是一个对象,也是指向了一个类,这样一想,其实也是合理的,在OC中,一切皆是对象,类当然也除外,那这个类对象是属于什么类的呢?

其实在OC中,还有一个meta-class的概念,一般把它叫做元类,我们上面说到的类,其实就是元类的一个实例,这里有点拗口,但是这个感念一定要清楚,那之前说的OC一切皆是对象,那元类也应该是一个实例,他也应该指向一个类,说的没错,我们来看一下OC中的类的继承图就清楚了

上面是我画的一个继承图,在图中,我们能够看出,所有的类都是继承自NSObject,NSObject的父类为nil,所有的类都是元类的实例,然后根元类,即是NSObject的元类,他的父类是NSObject,然后他自己是自己的实例,这样一来就形成了一个循环,正好OC 一切皆是对象。

当然,在我们平时开发中,基本不会接触到元类。但是我们也需要了解其中的具体原理。

我们再看看上面的类定义,其中还有其他的结构

1
2
3
4
5
6
7
8
9
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;

这其中包含了这个类的所有信息,其中我们常用到的就是属性列表,方法列表,还有实现的协议列表,其中有个cache,这个其实就是我们上面说到的快速缓存表,关于其中的属性列表和方法列表之类的,我在之前的一篇文章中提到过这些的应用,请移步
http://ppsheep.com/2016/07/25/Runtime详解/

理解消息转发机制

好,现在我们开始来讲一下消息转发,首先我们来看实例的消息转发,这里说的消息转发,其实是方法的调用,我们上面已经讲过了。
先来看例子

1
2
//首先我在VC里执行一个不存在的方法
[self performSelector:@selector(logInstanceMethod)];

这里需要说明一下,因为现在项目基本都会使用ARC,如果使用了ARC,那么如果我们直接像平时调用方法那样调用一个不存在的方法,编译器会报错,因为ARC其实是在我们编译阶段,插入了内存管理的代码,在运行期间,是没有ARC的,这个和Java就不一样了。没有方法,当然没办法进行引用计数,所以,编译会报错,但是使用performSelector,这个方法,我的理解是在运行期间,才会知道是否有这样一个方法,这个方法定义出来,就是为了runtime实现的。如果有同学详细了解过,麻烦告知一下。

然后我们在VC里面实现几个方法,这几个方法都在NSObject头文件中有声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector");
return [super forwardingTargetForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"forwardInvocation");
// [super forwardInvocation:anInvocation];
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod");
return [super resolveInstanceMethod:sel];
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"methodSignatureForSelector");
if ([self respondsToSelector:aSelector]) {
return [super methodSignatureForSelector:aSelector];
}else{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
}

先不做解释,我们运行一下,看看效果怎么样

1
2
3
4
2017-03-10 17:15:32.265 动态添加实例方法和类方法[29490:5570176] resolveInstanceMethod
2017-03-10 17:15:32.267 动态添加实例方法和类方法[29490:5570176] forwardingTargetForSelector
2017-03-10 17:15:32.267 动态添加实例方法和类方法[29490:5570176] methodSignatureForSelector
2017-03-10 17:15:32.268 动态添加实例方法和类方法[29490:5570176] forwardInvocation

程序并没有崩溃,而是有先后顺序调用了这几个方法,这里需要说明,如果使用storyBoard的同学,resolveInstanceMethod会调用两次,因为这个初始化是,会调用一个方法叫做 setStoryBoard,这个方法,需要在父类中才会处理,在当前是找不到的,所以会直接来调用。

好,来分析一下:

消息转发大致分为两个阶段:

  • 阶段一 动态方法解析:首先会先问当前类能否动态增加方法,来处理这个不能被处理的消息,这时候就会调用到方法resolveInstanceMethod:,在这个方法中,我们可以进行动态增加方法,来处理这个实例不能处理的方法,如果当前类不能够动态增加方法,那么直接返回NO,这时候就进入第二个阶段,完整的消息转发
  • 阶段二 完整消息转发:完整消息转发也分为几步 首先,类已经不能动态增加方法了,那么就需要找其他能够处理的类来处理这个方法,如果有其他对象能够处理这个消息,那么直接将这个消息发送给他,进行消息处理。如果没有找到能够处理的对象,那么进行第二步,到方法签名,然后将消息的所有细节全部封装到NSInvocation对象,进行最后的处理,如果还未能处理,那么将会调用到NSObject的doesNotRecgnized什么方法,不知道拼的对不对,意思就是没找到这个方法,然后崩溃。

大致的一个流程就是这样,也对应到我们的方法调用,这里为什么我们的没有崩溃呢,因为在forwardInvocation上,我没有调用super,这样就不会到NSObject的方法了

上面就是一个消息转发的全过程,接下来我们来讲动态增加实例方法。

动态增加实例方法

上面,我们讲了,动态增加实例方法,都是在resolveInstanceMethod方法中,那么我们来增加试试看

首先,我先自定义一个实例方法

1
2
3
- (void)myInstanceMethod{
NSLog(@"我的实例方法");
}

然后,我在动态解析方法时,将执行的logInstanceMethod,换成我的这个方法myInstanceMethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
处理实例方法

@param sel 需要动态添加的方法
@return 是否已经有可实现的方法
*/
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod");
IMP imp = [self instanceMethodForSelector:@selector(myInstanceMethod)];
if (sel == @selector(logInstanceMethod)) {
class_addMethod([self class], sel, imp, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

- (void)myInstanceMethod{
NSLog(@"我的实例方法");
}

其中class_addMethod有四个参数

1
2
3
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, 
const char *types)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

分别代表的含义是 需要给哪个类增加方法,增加的方法的名称,具体实现的方法地址,方法的返回值和参数

这里解释一下,关于方法的返回值和参数,是通过字符串来表示的,具体的,可以看一下这里

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100

这样,我们就给我们当前的类增加了一个名为logInstanceMethod的方法,他的实现,则是在方法myInstanceMethod里

这里需要注意一点,我们传Class的时候,传的是 [self class],这个返回的是当前实例所属的对象,后面我们再讲动态增加类方法的时候,还需要传一个,那就是meta-class,到时候我在讲,这里只是提一下,注意两者之间的区别

我们再来运行一下

1
2
2017-03-10 17:31:49.298 动态添加实例方法和类方法[31090:5856225] resolveInstanceMethod
2017-03-10 17:31:49.299 动态添加实例方法和类方法[31090:5856225] 我的实例方法

这里可以看到,到了动态解析,消息转发就已经完成了,并且,我们的方法,动态添加了进去

好今天,先讲到这里,消化一下,说的有点多,后面一篇文章,我将会讲到,将消息转发给其他对象处理,还有动态增加类方法。

欢迎大家关注我的公众号,我会定期分享一些我在项目中遇到问题的解决办法和一些iOS实用的技巧,现阶段主要是整理出一些基础的知识记录下来

上边是公众号,下边是我个人微信

文章也会同步更新到我的博客:
http://ppsheep.com