004-对象,类和元类

前言

上一篇我们根据底层源码,构想画了一张图。
那么他们的底层是如何实现的?
他们之间的关系又是什么样的呢?
带着疑问,让我们一步步来验证吧。

image.png

实例

通过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指针绑定关联的。
下图是苹果官方的经典的层次结构图示,与我们验证的结果相符。

isa流程图.png

为什么设计元类

虽然我们知道了 实例对象&类&元类的层次结构以及他们之间怎么关联的。但这里就有个疑问,为什么要设计元类这一层。

一个程序会有多个同类型的实例对象,他们的公共内容可以放在类模板中,比如对象方法,对象大小,有哪些属性,成员变量等。
但类对象只会有一个,为什么还要设计元类呢?类方法为什么不放在类的一个单独的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之间结构,对于面向对象的设计多了一些了解。其中有不足,不对的地方也希望积极留言。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 229,362评论 6 537
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,013评论 3 423
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 177,346评论 0 382
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,421评论 1 316
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,146评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,534评论 1 325
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,585评论 3 444
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,767评论 0 289
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,318评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,074评论 3 356
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,258评论 1 371
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,828评论 5 362
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,486评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,916评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,156评论 1 290
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,993评论 3 395
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,234评论 2 375

推荐阅读更多精彩内容