关于objc_runtime的消息机制(二)

接上篇,我们已经大概的聊完了c++的虚函数实现机制。间接寻址体现在虚函数表的实现上。虚函数表由编译负责帮我们维护。我们来回头捋一捋函数调用过程的变化。在c中 函数名直接被译为函数指针(地址),调用的过程就是直接跳转到目的地址执行(当然,这个跳转不是普通的命令跳转,还包含着cpu寄存器状态的压栈 等等,不做细谈。)。到了c++中,对虚函数的调用就有了间接寻址了,这个函数的调用过程包括了:
. 1 根据对象地址找到对象内存区域
. 2 取出位于对象内存头部空间的虚函数表基址
. 3 查表 找到虚函数的具体地址
. 4 有了函数地址 然后就可以调用了。
总之 ,间接无非就是建立寻址表,将直接的地址翻译变为间接寻址。有了c++的虚函数其实已经可以实现面向对象中一个重要的概念:多态。但是 程序员是能够轻易满足的么?显然不是啊。这哪够?虚函数表是编译器负责维护和实现的,程序员并不能直接的操作和更改(其实是可以的,毕竟我们知道了对象的首段地址就是基地址表,我们可以通过更改这个表来实现,但是这些都不是C++设计者的本意,且这样的代码写出来可读性性极差,更别说什么维护了)。
这就牵扯出本文将提及的终极大杀器(苹果大法好,不过我还是喜欢巨硬。)object c 被称作动态语言,这个动态该如何解释呢?还是用c来做对比。c中每一个标识符(变量)都必须有一个类型,基本类型 int char float 等等,自定义类型 struct。这个类型一经声明,其实该对象在内存中的空间模型也就固定了(内存空间模型包括所占字节的大小,以及自定义结构体中,各个字段的内存区域分配)。object c 不是这样的语言。id object = *(赋值号右边的对象是什么类型就是什么类型。先记住这句话 一会儿就能体会到)。 简单的推测,id或许是一个指针类型,因为指针所占字节数是恒定不变的啊,不管什么类型的指针都可以进行相互赋值(将32个字节的struct 赋值给int,多出来的28个字节也没地搁啊,所以说指针是程序设计中最成功的概念之一)。于是我们跳转到id的定义看到这样的代码

typedef struct objc_object *id;

果不其然 id 是一个指向objc_object结构体的指针。这时候引发了我们的好奇继续跳转到定义

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

看到objc_object中只有一个成员 Class类型的 isa。Class又是是什么呢

typedef struct objc_class *Class;

Class其实还是一个指针类型 指向的是objc_class结构体,这时候我们继续跳转到定义 可以看到objc_class结构体的定义

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

这就相当有趣了,这个结构体中的字段似乎是在描述一个'类'的信息,比如 name('类'的名称) version ('类'的版本号) 另外还要两个重要的成员

struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists 

根据名字 ,一个是成员变量列表 一个是方法列表。有了这些信息,对于一个有面向对象编程经验的码农,这些就可以描述一个类了呀。之所以前面用单引号的'类',是我故意要做区分。还记得刚接触面向对象语言时那种不知所云的感觉么。各种书上在描述类时经常会提到的字眼就是 设计模板 设计图纸。类就使我们用代码去描述的一个抽象,它会被编译器具象为内存中的实际对象。这时候我们就会有了这么一个思考,对啊,我们平时定义类,类信息是由编译器负责维护的。一旦编译完成,内存中关于类的很多信息就消失了(当然也完全没必要存在,比如类的名字)。为什么object c要大费周章的用一个结构体的变量的去保存这些信息呢?如果是从文章一开始就跟紧"计算机科学中,任何问题都可以通过增加一层间接寻址来实现"这一思路的读者,此时应该一眼洞穿apple的用意。动态啊,动态。每一个对象有一个指向它所属'类'的指针,这个'类'实际上是存在于内存中保存着类信息的结构体变量,不同于以往的类信息只是编译前安安静静的躺在编辑器中。有了这个'类'结构体变量,我们就真的可以为所欲为了。想想都美妙,本来一旦定义了一个类,类的声明代码在编译前一经确定,编译后就没它啥事了。现在是有了一个结构体保存这些信息,每个对象都有一个指向所属'类'的指针。我们一旦改变了结构体中的内容,是不是就改变了对象的'类'呢。结构体中有方法列表,我们给这个列表添加一个表象,是不是就等于为对象的'类'动态(因为这一切都是运行时能够进行的)的添加了一个方法呢。
现在我们就对了oc的动态性有了直观的感受,有了'类'结构体。我们看到的以下代码

NSUInteger lenght = [str length];

就不再是普通的函数调用了, 我们称之为消息发送。str 是这个消息的接受者。实际上以上的代码会被编译为

objc_msgSend(str, @selector(length));

这里,利用runtime为我们提供的 objc_msgSend函数,我们向str发送了一条消息,通过str中的isa指针找到'类'结构体,在结构体的方法列表中寻找到相应的函数实现,然后调用。事实上,如果该'类'的方法列表中没有搜寻到,还会继续的向父'类'去搜寻,请大家注意到Class _Nullable super_class OBJC2_UNAVAILABLE ,super_class 这个成员变量 它也是指向'类'结构体的指针,它指向的是当前对象所属'类'的父'类' 。总之消息就这样在这些继承关系的'类'中传递,直到找到合适的处理方法,或者最终也没有找到,跑出异常。其实有了将类的信息储存于内存中,实现了一层间接寻址的概念,其他的都是水到渠成的。文章并未提到元类的概念,请读者自行搜索别的文章读,有了这篇文章做铺垫,读起来也定会很轻松。

你可能感兴趣的:(关于objc_runtime的消息机制(二))