写在前面
通过前面篇章的探索,我们已成功的从对象过渡到类了.本文就来讲讲实例出实例对象的类以及类的结构.
一、类的本质
① 类的本质
objc源码下准备代码
利用
clang将OC文件输出cpp文件.(xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp)
发现类在底层用Class接收,然后开始大海捞针(开天眼模式)找到了Class的定义
想要找到objc_class就搜不到了,但是总觉得它似曾相似,或许能在objc源码中找到灵感
在源码中搜索代码的经验 "objc_class :"、 "objc_class {"
我们发现
objc_class继承于objc_object
结论:类的本质是objc_class类型的结构体,objc_class继承于objc_object,所以满足万物皆对象
② objc_class & objc_object 、objc_object和NSObject的关系
objc_object和NSObject
等等,为什么继承objc_object就满足万物皆对象了???
看过NSObject的定义就知道了
仔细比较的话就能看出
NSObject和objc_object有着说不清道不明的关系
- 其实
NSObject是objc_object的仿写,和objc_object的定义是一样的,在底层会编译成objc_object - 同理
NSObject类是OC版本的objc_class
objc_class & objc_object
前面使用clang编译过main.m文件,从编译后的c++文件中可以看到如下c++源码
-
NSObject的底层编译是NSObject_IMPL结构体- 其中
Class是isa指针的类型,是由objc_class定义的类型 - 而
objc_class是一个结构体.在iOS中,所有的Class都是以objc_class为模板创建的
- 其中
-
在
objc4源码中搜索objc_class的定义,源码中对其的定义有两个版本- 旧版 位于
runtime.h中,已经被废除 - 新版位于
objc-runtime-new.h,这个是objc4-818.2最新优化的,我们后面的类的结构分析也是基于新版来分析的从新版的定义中,可以看到
objc_class结构体类型是继承自objc_object的.
- 旧版 位于
-
在
objc4源码中搜索objc_object或者objc_object {,这个类型也有两个版本- 一个位于
objc.h,没有被废除,从编译的main-arm64.cpp中可以看到,使用的这个版本的objc_object - 另一个位于
objc-privat.h
- 一个位于
那么objc_class 与 objc_object 到底有什么关系?
通过上述的源码查找以及main-arm64.cpp中底层编译源码,有以下几点说明:
- 结构体类型
objc_class继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性 mian-arm64.cpp底层编译文件中,NSObject中的isa在底层是由Class定义的,其中class的底层编码来自objc_class类型,所以NSObject也拥有了isa属性NSObject是一个类,用它初始化一个实例对象objc,objc满足objc_object的特性(即有isa属性),主要是因为isa是由NSObject从objc_class继承过来的,而objc_class继承自objc_object,objc_object有isa属性.所以对象都有一个isa,isa表示指向,来自于当前的objc_objectobjc_object(结构体) 是 当前的根对象,所有的对象都有这样一个特性objc_object,即拥有isa属性
引申问题:objc_object 与 对象的关系
- 所有的
对象都是以objc_object为模板继承过来的 - 所有的对象是来自
NSObject(OC),但是真正到底层的是一个objc_object(C/C++)的结构体类型 objc_object与对象的关系 是继承关系
③ Class isa
isa明明是isa_t类型的,为什么注释了一句Class ISA?
- 万物皆对象,用继承于
objc_object的Class接收是没问题的 - 强转,方便
isa走位时返回类的类型
④总结
- 所有的
对象 + 类 + 元类都有isa属性 - 所有的
对象都是由objc_object继承来的 - 简单概括就是
万物皆对象,万物皆来源于objc_object,有以下两点结论:- 所有以
objc_object为模板创建的对象,都有isa属性 - 所有以
objc_class为模板创建的类,都有isa属性
- 所有以
- 在结构层面可以通俗的理解为
上层OC与底层的对接:下层是通过结构体定义的模板,例如objc_class、objc_object上层是通过底层的模板创建的一些类型,例如TCJPerson
objc_class、objc_object、isa、object、NSObject等的整体的关系,如下图所示
二、指针内存偏移
在分析类结构之前,需要先了解内存偏移,因为类信息中访问时,需要使用内存偏移
① 普通指针 - 值拷贝
我们观察上面的代码,虽然整型变量
a和b都是被赋值为10,但是a和b内存地址是不一样的,这种方式被称为值拷贝.
② 对象 - 指针拷贝或引用拷贝
通过运行结果,可以知道obj1和obj2对象不光自身内存地址不一样,连指向的对象的内存地址也不一样,这种方式被称为指针拷贝或引用拷贝.
我们可以用一幅图来总结上面的两个例子:
③ 用数组指针引出 - 内存偏移
通过运行结果可以看到:
&a和&a[0]的地址是相同的.即首地址就代表数组的第一个元素的地址.- 第一个元素地址
0x7ffeefbff400和第二个元素地址0x7ffeefbff404相差4个字节,也就是int的所占的4字节,因为他们的数据类型相同. d、d+1、d+2这个地方的指针相加就是偏移地址.地址加1就是偏移,偏移一个位数所在元素的大小.- 可以通过地址,取出对应地址的值.
三、类的结构
从objc_class的定义可以得出,类有4个属性:isa、superclass、cache、bits
① Class ISA
不但实例对象中有isa指针,类对象中也有isa指针关联着元类
Class本身就是一个指针,占用8字节
② Class superclass
顾名思义就是类的父类(一般为NSObject)superclass是Class类型,所以占用8字节
③ cache_t cache
虽然对这个属性比较陌生(后面章节会详细介绍),但是cache在英文中的意思是缓存
cache_t是一个结构体,内存长度由所有元素决定:_bucketsAndMaybeMask是long类型,它是一个指针,占用8字节;
mask_t是个uint32_t类型,_mask占用4字节;因_occupied和_flags都是uint16_t类型,uint16_t是 unsigned short 的别名,所以_occupied占用2字节;_flags占用2字节
=>cache_t占用16字节
④ class_data_bits_t bits
又是一个陌生的属性,但是苹果工程师还是蛮友好的,这一看就是存数据的地方
那么问题来了,类的属性方法都去哪儿了?是在cache还是在bits? 其实前文中有提到一丢丢——objc_class中有个class_rw_t *data()方法
通过前面的分析可知,想要获取bits的中的内容,只需通过类的首地址平移32字节即可.
class_rw_t是在运行时生成的,它在realizeClass中生成,它包含了class_ro_t.它在_objc_init方法中关于dyld的回调的map_images中最终将分类的方法与协议都插入到自己的方法列表、协议列表中.它不包含成员变量列表,因为成员变量列表是在编译期就确定好的,它只保存在class_ro_t中.不过,class_rw_t中包含了一个指向class_ro_t的指针.
四、类的属性方法
为TCJPerson添加hobby成员属性、tcj_name属性变量、sayNB类方法、sayHello实例方法
① 类的属性
x/4gx cls打印当前类结构
bits刚好是类的内存首地址+isa、superclass、cache的内存长度
=> 0x1000082b0+32字节 = 0x1000082d0
po打印不出来,那就类型强转打印输出bits的内存地址
根据class_rw_t *data() { return bits.data(); }打印bits.data()
从$4指针的打印结果中可以看出bits中存储的信息,其类型是class_rw_t,也是一个结构体类型.但我们还是没有看到属性列表、方法列表等,需要继续往下探索.
通过查看class_rw_t定义的源码发现,结构体中有提供相应的方法去获取属性列表、方法列表等,如下所示
接下来继续探索
bits中的属性列表,以下是 LLDB 探索的过程
那么我们的hobby去哪了呢?难道我TCJPerson类只配拥有“国籍”不配拥有“姓名”?让我静静...
由此我们知道class_rw_t中的property_list 中只有属性,没有成员变量,属性与成员变量的区别就是有没有set、get方法,如果有,则是属性,如果没有,则是成员变量.
在一顿猛如虎的操作(开天眼)之后,发现了class_rw_t有个属性class_ro_t.
class_ro_t是编译期生成的,它存储了当前类在编译期就已经确定的属性、方法以及协议,它里面是没有分类中定义的方法和协议的.
在控制台输出ro,跟class_ro_t的结构类型一摸一样.
而我们的成员变量就在ro的ivars里面,好期待啊
如愿拿到了hobby和tcj_name,但是这个tcj_name长得有点不一样.
仔细想想这不就是编译器会在底层自动将属性变量生成一个成员变量 _tcj_name(_前缀+属性变量),嗦嘎撕裂,还有谁...
小总结:
- 通过
{}定义的成员变量,会存储在类的bits属性中,通过bits --> data() -->ro() --> ivars获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量 - 通过
@property定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含属性
② 类的方法
-
类的实例方法存储在类的bits属性中,通过bits --> methods() --> list获取实例方法列表,例如TCJPersong类的实例方法sayHello就存储在TCJPerson类的bits属性中,类中的方法列表除了包括实例方法,还包括属性的set方法和get方法,还有系统在底层添加了一个c++的.cxx_destruct方法.我们来打印验证一方 -
类的类方法存储在元类的bits属性中,通过元类bits --> methods() --> list获取类方法列表,例如TCJPerson中的类方法sayNB就存储在TCJPerson类的元类(名称也是TCJPerson)的bits属性中.打印验证一下 -
类的类方法可以理解成元类对象的实例方法,因此存在元类中.
③ 结论
- 成员变量存放在
ivar - 属性存放在
property,同时也会存一份在ivar,并生成setter、getter方法 - 对象方法存放在
类里面 - 类方法存放在
元类里面
④ API验证
利用底层开放的API可以验证以上结论
五、提出疑问
① 类存在几份?
由于类的信息在内存中永远只存在一份,所以 类对象只有一份.
② objc_object 与 对象的关系
- 所有的
对象都是以objc_object为模板继承过来的 - 所有的对象 是 来自
NSObject(OC),但是真正到底层的 是一个objc_object(C/C++)的结构体类型 objc_object与对象的关系是继承关系
③ 什么是 属性 & 成员变量 & 实例变量 ?
属性(property):在OC中是通过@property开头定义,且是带下划线成员变量+setter+getter方法的变量成员变量(ivar):在OC的类中{}中定义的,且没有下划线的变量实例变量:通过当前对象类型,具备实例化的变量,是一种特殊的成员变量,例如NSObject、UILabel、UIButton等
④ 成员变量 和 实例变量什么区别?
实例变量(即成员变量中的对象变量就是实例变量):以实例对象实例化来的,是一种特殊的成员变量NSString是常量类型, 因为不能添加属性,如果定义在类中的{}中,是成员变量成员变量中除去基本数据类型、NSString,其他都是实例变量(即可以添加属性的成员变量),实例变量主要是判断是不是对象
⑤ isKindOfClass 和 isMemberOfClass 的理解
这是一道涉及isa走位图的面试题,大胆猜测下结果
先来探索一下
isKindOfClass和isMemberOfClass的实现
上图中我已做了详细的解析.那么实现情况是怎样的呢?
通过断点调试,isMemberOfClass 的类方法 和 实例方法的流程是正常的,会走到上面分析的源码中,而isKindOfClass根本不会走到上面分析的源码中(!!!注意这里,这是一个坑点),而是会走到下面这个源码中,其类方法和实例方法都是走到objc_opt_isKindOfClass方法源码中
-
汇编调用如下
-
objc_opt_isKindOfClass方法源码如下
为什么会这样呢?主要是因为在LLVM中编译时对其进行了优化处理.
所以调用objc_opt_isKindOfClass实际走的逻辑如图所示
接下来在结合isa走位图(实线为父类走向)可以得出前面四个打印结果:
- NSObject元类与NSObject类不相等,NSObject元类的父类(指向NSObject类)与NSObject类相等——YES
- NSObject元类与NSObject类不相等——NO
- TCJPerson元类与TCJPerson类不相等,TCJPerson元类的父类与TCJPerson类不相等——NO
- TCJPerson元类与TCJPerson类不相等——NO
后面四个结果分析如下:
- NSObject类与NSObject类相等——YES
- NSObject类与NSObject类相等——YES
- TCJPerson类与TCJPerson类相等——YES
- TCJPerson类与TCJPerson类相等——YES
六、补充知识
strong & copy & weak 底层分析
在clang编译的cpp文件中可以发现 strong & copy & weak 修饰的属性在编译的底层代码中是有区别的
- 在
TCJPerson中我们定义了两个两个属性,分别用copy和strong修饰 - 用
clang将main.m文件编译成main-arm64.cpp,然后发现copy和strong修饰的属性的set方法是有区别的
这里就有疑问了,为什么copy修饰的属性使用了objc_setProperty,而strong修饰的没有?
- 在
LLVM中搜索objc_setProperty,找到如下所示的getOptimizedSetPropertyFn方法从这里即可看出,针对不同的修饰符,返回的是不同的
- 如果是
atomic & copy修饰,name为objc_setProperty_atomic_copy - 如果是
atomic且没有copy修饰,name为objc_setProperty_atomic - 如果是
nonatomic & copy修饰,name为objc_setProperty_nonatomic_copy - 其他剩余的组合,即
nonatomic、nonatomic & strong、nonatomic & weak等,name为objc_setProperty_nonatomic
上述的几个name分别对应objc4-818.2源码中的如下方法
然后通过汇编调试发现,最终都会走到objc_storeStrong
-
copy修饰的属性汇编调试结果 -
strong修饰的属性汇编调试结果 -
源码中搜索
objc_storeStrong,有如下源码,主要也是retain新值,release旧值 -
llvm编译源码中搜索objc_storeStrong,找到EmitARCStoreStrongCall方法,如下图所示,发现copy和strong修饰的属性执行的策略是不一致的 -
llvm中搜索EmitARCStoreStrongCall方法,在GenerateCopyHelperFunction方法有调用,然后在这里发现了strong和weak的不同处理其中
BlockCaptureEntityKind有如下的枚举值以及表示的含义 -
如果是
weak修饰,执行EmitARCCopyWeak方法,如下所示,weak在底层的调用是objc_initWeak -
如果是
strong修饰,执行EmitARCStoreStrongCall方法
结论
copy和strong修饰的属性在底层编译的不一致,主要还是llvm中对其进行了不同的处理的结果.copy的赋值是通过objc_setProperty,而strong的赋值时通过self + 内存平移(即将指针通过平移移至name所在的位置,然后赋值),然后还原成strong类型strong & copy在底层调用objc_storeStrong,本质是新值retain,旧值releaseweak在底层调用objc_initWeak
② Type Encoding & Property Type String
Type Encoding-官方文档 Property Type String-官方文档
clang中的方法签名
Type encoding -- clang编译后,方法列表的这些字符的含义是什么?
以@16@0:8为例
@16表示返回字符串占用16个字节-- 第二个@占8字节,sel占8字节- 第一个
@表示返回值 16表示总共占用的字节数16字节- 第二个
@:第一个参数id--@统配类型typedef struct objc_object *id
0--从0开始0-8占8字节:-- 代表sel,方法编号8--从8开始8-16占8字节
- 第一个
- 而
v24@0:8@16中的v -- void无返回值
更多的可以查看官网的以下列表
clang编译后的属性的attribute
clang编译输出了属性的attribute,同样也可以通过property_getAttributes方法获取
T表示type@表示变量类型C表示copyN表示nonatomicV表示variable变量,即下划线变量_tcj_name
更多的可以查看官网的以下列表
写在后面
和谐学习,不急不躁.我还是我,颜色不一样的烟火.