前言
上一篇我们根据底层源码,构想画了一张图。
那么他们的底层是如何实现的?
他们之间的关系又是什么样的呢?
带着疑问,让我们一步步来验证吧。
实例
通过c++源码,看下实例对象的底层结构
根类实例对象
struct NSObject_IMPL {
Class isa;
};
QPPerson实例对象-继承自NSObject
struct QPPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString * _Nonnull _publicName;
NSString *_privateName;
};
QPCoder实例对象-继承自QPPerson
struct QPCoder_IMPL {
struct QPPerson_IMPL QPPerson_IVARS;
int _level;
};
可以看到,
1.实例对象直接持有各自成员变量
2.并通过strcut结构体嵌套的方式从父类实例结构体那里继承父类实例的成员变量
3.所有的OC实例对象都会从根对象objc_object实例结构体那里得到他们的第一个成员变量isa。isa指针指向其类型cls。
4.通过验证发现父类的私有成员变量也会被子类继承(结构体嵌套),无法显式访问(不暴露在.h文件中),但可以通过KVC访问父类的私有成员变量。
截屏2021-10-18 下午3.57.04.png
实例对象的创建
在前面的文章中,探索alloc函数,我们知道了一个实例对象的创建过程。首先alloc函数从cls模板中获取了一个实例结构体所需要的大小,并调用calloc函数返回所需大小的内存空间,然后initIsa。在initISa中,把isa跟cls绑定,并设置了一些位(引用计数&hasCxx等)。
在这里并没有直接使用到上面看到QPPerson 或者 QPCoder的结构体,那么这些结构体有什么作用?
继续看我们的c++源码
static struct _class_ro_t _OBJC_CLASS_RO_$_QPPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
0, __OFFSETOFIVAR__(struct QPPerson, _publicName), sizeof(struct QPPerson_IMPL),
(unsigned int)0,
0,
"QPPerson",
(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_QPPerson,
0,
(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_QPPerson,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_QPPerson,
};
extern "C" unsigned long int OBJC_IVAR_$_QPPerson$_publicName __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct QPPerson, _publicName);
static NSString * _Nonnull _I_QPPerson_publicName(QPPerson * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_QPPerson$_publicName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_QPPerson_setPublicName_(QPPerson * self, SEL _cmd, NSString * _Nonnull publicName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct QPPerson, _publicName), (id)publicName, 0, 1); }
QPPerson_IMPL的结构体信息,会被类保存,包括:结构体的大小、setter、getter、成员变量的相对偏移量、属性修饰等等,这些信息都会被类模板cls保留。
在运行时,我们通过cls创建实例对象,并将实例对象instance的isa和cls绑定。instance直接持有实例变量,并可以通过cls模板中的getter、setter或者直接通过self+相对偏移量访问它的成员变量。(关于getter&setter&成员变量的访问,后面单独写一篇来讲讲)
小结:
1.instance直接持成员变量和isa,通过isa绑定了cls。
2.isa是实例成员和cls联系的桥梁(甚至我们可以修改isa指向来达到修改其类的效果)。
3.cls模板保存了实例结构体构造,属性成员的getter、setter,实例大小,成员变量的相对偏移量,方法等多个实例可以共享的信息。
4.因此,实例instance的大小只跟成员变量的数量及大小相关。
那么cls的底层是如何实现的,元类&父类这些结构又是如何串联起来的呢?
类
上面我们研究了实例对象的数据结构。我们知道,所有的instance都会从根实例结构体objc_object那里继承isa指针(结构体嵌套),指向其类。那么类的底层实现是什么样的呢?
看下源码:
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
---省略方法---
}
类的实现还是比较复杂,后面单独写文章分析,这里主要想探索下,实例&类&元类他们是怎么样关联起来的,它们各自的功能是什么,为什么这样设计?
从源码我们看到,struct objc_class继承自objc_object,由此可知
1.objc_class结构体跟实例instance一样都是objc_object,都是对象
2.objc_class也有isa指针
3.objc_class有一个superClass成员
4.跟instace不同,所有的objc_class是统一结构体的模板,拥有相同成员。
我们已经知道实例instance的isa指向objc_class,那么objc_class的isa指针是不是就是指向元类?superClass指针是不是指向父类?
截屏2021-10-19 下午7.55.08.png
QPPerson的isa值为0x1000083a0等于metaClass的地址
QPPerson的superClass成员的值为0x7fff806a4088等于superClass的地址,也就是NSObject
总结:
1.类的isa指向元类
2.类的superClass指向父类
元类
元类的指针类型也是Class。
也就是说元类和类是同样的数据结构,也有isa指针和superClass指针。
那么元类的isa指针 和 superClass指针指向谁呢?
截屏2021-10-19 下午8.10.58.png
可以看到:
QPCoder的元类的isa = 0x00007fff806a4060
QPPerson的元类的isa = 0x00007fff806a4060
NSObject的元类的isa = 0x00007fff806a4060
NSObject元类的地址 = 0x00007fff806a4060
结论1:所有元类的isa都指向了根类的元类
QPCoder元类的superClass = 0x00000001000083a0 = QPPerson元类的地址
QPPerson元类的superClass = 0x00007fff806a4060= NSObject元类的地址
结论2:元类的superClass指向父类的元类
这里有个特殊的边界情况,根类的superClass,根类元类的superClass和isa该指向哪里?
通过上面的LLDB打印,可知:
NSObject的superClass= 0x0000000000000000,也就是nil
NSObject-meta元类的isa = 0x00007fff806a4060,也就是指向自己,符合我们上面的总结,所有元类的isa都指向根类的元类。
NSObject-meta的superClass = 0x00007fff806a4088 = NSObject。根元类的superclass竟然指向了根
类。
结论3:根类的superClass为nil,根元类的superClass为根类,所有元类的isa都指向根元类,包括根元类自己
小结
通过上面源码,已经LLDB的打印验证,我们验证了实例对象&类&元类的关系及层次结构,以及他们是怎样同isa和superClass指针绑定关联的。
下图是苹果官方的经典的层次结构图示,与我们验证的结果相符。
为什么设计元类
虽然我们知道了 实例对象&类&元类的层次结构以及他们之间怎么关联的。但这里就有个疑问,为什么要设计元类这一层。
一个程序会有多个同类型的实例对象,他们的公共内容可以放在类模板中,比如对象方法,对象大小,有哪些属性,成员变量等。
但类对象只会有一个,为什么还要设计元类呢?类方法为什么不放在类的一个单独的list里面跟对象方法区分?元类还有存在的必要吗?
当我们po LGPerson.class的时候返回仍然是LGPerson自己。很显然苹果也不想对外暴露元类的存在,不希望通过API去访问元类。那他为什么还要设计元类呢。
带着这个疑问,翻阅了一些资料,做下总结。
- 首先,这么做符合面向对象的设计理念,万物皆对象,class也是对象,有它所属的类,即metaClass。
- 职责分离,每个对象有各自的class,每个class有各自的metaClass
- 复用objc_msgSend通道,在这个层面上class就是一个普通对象,通过同样的通道到它所属类metaClass中查找响应方法。
- 类方法不仅实现实现专门的实例化器(与其他一些面向对象语言的“构造函数”大致对应),且有利于类之间共享的行为
- metaclass是 smalltack 中为数不多丑陋设计之一,好处是不需要了解工厂模式,类本身就能充当工厂
- 综上,也许我们不是必须为类设计元类,但是metaClass符合面向对象对象的设计理念,符合smallTalk消息传递的精髓。
metaClass的isa指向rootMetaClass
很显然我们的类设计的层次结构不能无限循环下去
- meta-class的存在使我们类实例的方法有地方存储。而元类本身的存在,苹果很显然是不想暴露的,也没有元类的方法给我们访问。那么元类的isa指针指向可以特殊设计,一般情况下它也不会被开发者使用到。
- 苹果把元类isa的指针指向了root-meta-class,也就是NSObject的元类-(包括根元类自己的isa指针,自己指向自己)。元类的isa似乎没什么用,只是让他们统一指向一块内存。
为什么根元类的superClass指针指向NSObject
- 无论元类还是类的底层数据结构都是objc_class,objc_class继承自objc_object。这也是面向对象的设计理念的体现。
- 复用objcMsgSend消息机制的通道,无论消息接收者是对象还是类对象,他们的方法查找路径有了一个统一的终点。
总结
- 对象直接持有成员变量和isa,对象的isa指向其所属类class
- class类模板保存共享信息
- class和metaClass都是objc_class类型的结构体
- class的isa指向元类,superClass指针指向父类
- metaClass的isa指针统一指向rootMetaClass,superClass指针父类的metaClass
- 根元类的superClass指向根类NSObject,NSObject的superClass为nil
- 元类的设计也许不是必要,但符合面向对象的设计理念,也符合smallTalk的消息传递的机制。
一点思考
学而时习之,温故而知新。特别是尝试去思考苹果为什么这样设计instance,class和metaclass之间结构,对于面向对象的设计多了一些了解。其中有不足,不对的地方也希望积极留言。
网友评论