iOS中OC对象模型的理论知识过一遍
以前学习化学知识的时候,有一句经典的话就是:“结构决定性质”。
这句话在软件开发中依然适用,不管是日常的业务开发工作,还是想探索下OC的底层原理,都离不开结构的限制。
本文是记录OC对象模型的结构设计。
==第一部分==
OC是一门面向对象的编程语言,每一个对象都是一个类的实例。在OC语言的内部,每一个对象都有一个名为isa的指针,指向该对象的类。每一个类描述了一系列它的实例的特点,包括成员变量的列表、成员函数的列表等。每一个对象都可以接受消息,而对象能够接受的消息列表保存在它所对应的类中。
按照面向对象语言OOP的设计原则,所有的事物都应该是对象,严格来说,OC并没有完全做到这一点,因为它有像int、double这样的简单变量类型,而类似Ruby一类的语言,连int变量也是对象。
因为类也是一个对象,所以它也必须是另一个类的实例,这个类就是元类(metaclass)。元类保存了类方法的列表。当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则该元类会向它的父类查找该方法,这样可以一直找到继承链的源头。
元类也是一个对象,那么元类的isa指针又指向哪里呢?为了设计上的完整,所有的元类的isa都会指向一个根元类(root metaclass)。根元类本身的isa指针指向自己,这样就形成了一个闭环。前面所提到的,“一个对象能够接受的消息列表是保存在它所对应的类中的”。在实际编程中,我们几乎不会遇到向元类发消息的情况,因此元类的isa指针很少在实际中用到。不过,设计“根元类”是为了保证面向对象概念在OC语言中的完整,即语言中的所有事物都是对象,都有isa指针。
根据isa指针这条线路,基本可以得出以下图示:
如果把类的实例看成是一个C语言的结构体struct,那么上面提到的isa指针就是这个结构体的第一个成员变量,而类的其他成员变量依次排列在结构体中,其他的成员变量包括类本身的成员变量,和从父类链继承过来的成员变量。因为对象在内存中的排布可以看成是一个结构体,该结构体的大小并不能动态变化,所以无法在运行时动态地给对象增加成员变量。这就是为什么分类category只可为对象增加成员方法,却不能增加成员变量的原因。而动态更改isa指针的应用场景之一就是KVO的实现。
虽然通过objc_setAssociatedObject和objc_getAssociatedObject方法可以变相地给对象增加成员变量,但由于实现机制不一样,所以并不是真正改变了对象的内存结构。
正是因为isa指针和method指针的类型是指针,多以可以动态地修改指针的值,从而做到对isa sizzling和method swizzling的支持。
与实例对象中的成员变量不一样,对象的方法定义都保存在类的可变区域中。OC 2.0 并未在头文件中将实现暴露出来,但在OC 1.0 中,我们可以看到方法的定义列表是一个名为methodLists的指针的指针。通过修改该指针指向的指针的值,就可以动态地为某一个类增加成员方法。这也是分类category实现的原理。
在OC的高级语法中,比如使用runtime运行时更改系统方法的定义、KVO的实现、消息的转播等等,都是对isa指针和method指针的利用。当然,runtime运行时的作用远远不止这些应用场景。
==第二部分==
上面提到的类、元类、根元类,加上根类NSObject是有继承关系的。由于类方法的定义时保存在元类中,而方法调用的规则是,如果该类没有一个方法的实现,则向它的父类继续查找。所以,为了保证父类的类方法在子类中可以被调用,所有子类的元类都会继承父类的元类,换而言之,类对象和元类对象有着同样的继承关系。
根据继承这条关系线路,可以得到下面的图示:
==第三部分==
如果想进一步探索底层的一些结构,可以在Xcode中按快捷键 Shift + Cmd + O,然后输入“NSObject”、“objc.h”、“runtime.h”。
在调试时,可以运行到“断点”处后,在Console中输入(比如:p *child)进行对象模型结构的查看。