OC底层原理10:类 & 类结构分析
前言
本章主要的目的是分析 类
和 类结构
objc_class 和 objc_object
为什么 对象
和 类
都有 isa指针
?
在 iOS-OC底层原理08:isa和类关联探索 中,使用 clang
编译过 main.m
文件,从编译后的 main.cpp
文件中,我们分析了 对象
的 本质
是 结构体
,如下:
NSObject
的底层编译是struct objc_object
结构体
1 | typedef struct objc_object NSObject; |
由编译后的 c++ 源码可知:
NSObject
底层被编译成结构体struct objc_object
类型,因此对象
的本质是结构体
isa指针
是Class
类型的,是由struct objc_calss
结构体定义的类型,所有的Class
都是以objc_class
为模板创建的objc_object
结构体内部有objc_class
这个结构体,那么问题来了
【问题】objec_class
和 objc_object
有什么关系呢?
我们通过查看 objc4源码
找到 objc_class
和 objc_object
的定义,来分析一下两者之间的关系:
在
objc4
源码中搜索objc_class
,其源码定义如下:- 一个位于
runtime.h
文件中,已经被废弃了
- 一个位于
- 一个位于
objc-runtime-new.h
文件中,是可用的
在
objc4
源码中搜索objc_object
,其源码定义如下:- 位于
objc.h
文件中
- 位于
通过上述查找源码和其定义,可总结出以下几点内容:
结构体类型
objc_class
继承自objc_object
类型,其中objc_object
也是一个结构体,且有一个isa
属性,所以objc_class
也拥有了isa
属性mian.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_object
objc_object(结构体
) 是当前的
根对象,所有的对象
都有这样一个特性objc_object
,即拥有isa属性
【百度面试题】objc_object 与 对象的关系?
所有的
对象
都是来自NSObject(来自于OC端)
,但是真正到底层
是一个objc_object(C/c++)
结构体类型的
所有的
对象
都是以objc_object
模板继承
过来的
【总结】:
所有的
对象 + 类 + 元类
都有isa
所有的
对象
都是由objc_object继承
来的简单概括就是
万物皆对象
,万物皆来源于objc_object
,有以下两点结论:- 所有以
objc_object
为模板创建的对象
,都有isa属性
- 所有以
objc_class
为模板创建的类
,都有isa属性
- 所有以
在结构层面可以通俗的理解为
上层OC
与底层
的对接:- 底层是通过
结构体
定义的模板
,例如objc_class、objc_object
上层
是通过底层的模板创建的一些类型,例如ZJPerson
- 底层是通过
补充知识-内存偏移
在分析 类信息
中存储哪些信息之前,需要先 了解内存偏移
,因为分析 类信息
需要用到内存偏移
- 【普通指针】
定义一个方法
1 | // 普通指针 |
在mian.m调用,查看打印结果:
1 | 2020-10-04 13:41:07.378260+0800 KCObjc[23781:838866] 10---0x7ffeefbff4a8 |
由上述的打印结果可知:
a、b
都是常量型变量
,a、b
的地址指针不一样
,分别指向值10
,在内存中10
是通过值拷贝
,分别赋值给a、b
a
的地址是0x7ffeefbff4a8
,b
的地址是0x7ffeefbff4ac
,他们相差4字节
,这是由a/b
本身类型(Int占4个字节
) 决定的
其地址指针指向如图所示:
- 【对象指针】
定义一个方法
1 | // 对象指针 |
在mian.m调用,查看打印结果
1 | 2020-10-04 14:14:59.341733+0800 KCObjc[24079:859357] <ZJPerson: 0x101035830>---0x7ffeefbff4a0 |
由上述的打印结果可知:
p1、p2
是一级地址指针
,p1、p2
是指向[ZJPerson alloc]
申请内存空间&p1、&p2
是二级指针
,&p1、&p2
指向p1、p2
对象的一级地址指针
其地址指针指向如图所示:
- 【数组指针】
定义一个方法
1 | // 数组指针 |
在mian.m调用,查看打印结果
1 | 2020-10-04 14:38:14.720430+0800 KCObjc[24171:872712] 0x7ffeefbff490 -- 0x7ffeefbff490 - 0x7ffeefbff494 |
由上述的打印结果可知:
&c
和&c[0]
都是取首地址
,即数组名等于首地址&c
与&c[1]
相差4个字节
,地址之间相差的字节数,主要取决于
存储的数据类型
*d
代表取c
的地址,可以通过lldb
调试p *(d+1)、*(d+2)
获取地址
所对应的值2、4
- 可以通过
首地址+偏移量
取出数组中的其他元素,其中偏移量是
数组的下标
,内存中首地址实际移动的字节数
等于偏移量 * 数据类型字节数
其地址指针指向如图所示:
探索类信息中都有哪些内容
由前文可知,所有的 类
都是以 objc_class
模板创建的,通过 objc4
源码可知,objc_class
中包含许多属性,如下:
1 | struct objc_class : objc_object { |
【这些属性的定义】:
isa
属性:继承自objc_object
的isa,占8
字节superclass
属性:Class
类型,Class
是由objc_object
定义的,是一个指针,占8
字节cache
属性:cache_t
类型bits
属性:简单从类型class_data_bits_t
目前无法得知,而class_data_bits_t
是一个结构体类型,结构体的内存大小
需要根据内部的属性
来确定,而结构体指针才是8字节
,只有首地址
经过上面3个属性
的内存大小总和
的平移
,才能获取到bits
结论:
我们知道了objc_class
中存储了类
的信息
,那么如何去验证呢?
【验证方法】lldb 调试验证:
- 自定义一个ZJPerson类,继承自NSObject
1 | @interface ZJPerson : NSObject |
- 在
main.m
中初始化ZJPerson
类和NSObject
类
1 | int main(int argc, const char * argv[]) { |
- 下断点到
NSObject...
行,通过lldb
调试,过程如下:
计算cache_t的内存大小
由上面的分析,我们知道了 cache_t
内存大小 和 内部属性
有关,因此我们需要进入 objc4-781新版本
内部源码去分析
进入 cache
类 cache_t
的定义(只贴出了结构体中 非static
修饰的 属性
,主要是因为 static类型
的属性 不存在结构体
的内存中),有如下几个属性:
1 | struct cache_t { |
计算 cache_t
内部属性的大小,最后的内存大小总和都是 16
字节
_buckets
类型是struct bucket_t *
,是结构体指针类型,占8字节
mask
是mask_t
类型,而mask_t
是unsigned int
的类型,占4字节
_flags
是uint16_t
类型,uint16_t
是unsigned short
的类型,占2个字节
_occupied
是uint16_t
类型,uint16_t
是unsigned short
的类型,占2个字节
总结:所以最后计算出 cache_t
类的内存大小 = 8 + 4 + 2 + 2 = 16字节
获取bits
由上述计算 Class类型 ISA 是 8字节
、Class superclass 是 8 字节
、cache_t cache 是 16字节;
可知,想要获取 bits
的中的内容,只需通过 类的
首地址平移 32字节(8 + 8 + 16)
即可
【验证】:通过lldb命令调试
自定义两个类 ZJPerson
和继承自ZJPerson的 ZJStudent
类:
1 | @interface ZJPerson : NSObject |
main.m中定义两个类
1 | ZJPerson * person = [ZJPerson alloc]; |
下断点到 NSLog
,lldb
命令调试 bits
流程如下:
由调试结果可知:
通过
p/x ZJPerson.class
获类的信息,即首地址
信息通过
x/4gx 0x0000000100002188
拿到首地址0x100002188
,经过32位平移
得到bits
地址0x1000021a8
通过
p (class_data_bits_t *)0x1000021a8
获取bits
通过
p $1->data()
获取class_rw_t
- 如果
class_data_bits_t *
是*
号,说明是对象,访问对象用->
- 如果
class_data_bits_t
是结构体
,用.
号
- 如果
通过
p *$2
打印bits
中class_rw_t
内部信息,firstSubclass = ZJStudent
,表示第一个继承子类ZJStudent
class_rw_t
通过查看 class_rw_t
定义的源码发现,结构体中有提供相应的方法去获取 属性列表
、方法列表
等,如下所示:
属性列表(property_list)
【准备工作】:在ZJPerson中增加一个 属性
和一个 成员变量
1 | @interface ZJPerson : NSObject |
【探索】 由上图可知 properties
是 property_array_t
类型的,其内部源码如下:
我们通过 lldb
调试属性列表,如下:
由lldb调试可知:
p $3.properties()
命令中的propertoes
方法是由class_rw_t
提供的,方法中返回的实际类型为property_array_t
由于
list
的类型是property_list_t
,是一个指针,所以通过p *$4
获取内存中的信息,同时也证明bits
中存储了property_list
,即属性列表p $6.get(0)
,获取ZJPerson
中的第一个属性name
p $6.get(1)
,想要获取ZJPerson
中的成员变量sex
, 发现会报错,提示数组越界了,说明property_list
中只有 一个属性name
【问题】探索成员变量的存储
由此可得出 property_list
中只有 属性
,没有 成员变量
,属性与成员变量的区别就是 有没有set、get方法
,如果 有
,则是 属性
,如果 没有
,则是 成员变量
。
那么问题来了,成员变量
存储在哪里?为什么会有这种情况?请移至文末的分析与探索
方法列表(methods_list)
【准备工作】 在ZJPerson中增加两个方法,一个对象方法和一个类方法
1 | @interface ZJPerson : NSObject |
【开始探索】通过 lldb
调试来获取方法列表,步骤如图所示:
由lldb调试可知:
系统在编译的时候自动帮你生成
通过
p $4.methods()
获得具体的方法列表的list结构
,其中methods
也是class_rw_t
提供的方法通过
p *$6.
打印的count = 4
可知,存储了4
个方法,其中syaHello
是自己
声明的方法,cxx_destruct、name、setName:
是系统
生成的方法可以通过
p $7.get(i)内存偏移
的方式获取单个方法,i
的范围是0-3
如果在打印
p $7.get(4)
,获取第五个方法,也会报错,提示数组越界
补充知识-属性、成员变量、实例变量
我们通常都知道 @property
是 属性
的意思,但是什么是 成员变量
和 实例变量
呢?我们探索一下:
1、首先我们自定义一个类 ZJPerson
,放在 main.m
中:
1 | // 成员变量 vs 属性 |
2、我们通过 clang
编译一下这个 main.m
为 main.cpp
,来查看 C
的底层实现,步骤如下:
1)我们打开终端,cd到你要编译文件的文件夹下:
1 | mac@192 ~ % cd /Users/mac/Desktop/逻辑教育/LG-OC底层大师班上课资料/20200914-大师班第5天-类原理分析-资料/01--课堂代码/001-类的属性与变量/001-类的属性与变量 |
2)输入编译命令clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk xxxx.m
编译你的目标文件:
1 | mac@192 001-类的属性与变量 % clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m |
3)然后你目录下就会重写一个 cpp
文件,内容比较多你可以搜索关键类 ZJPerson
对照查看:
3.我们打开 main.cpp
查找 ZJPerson
来查看一下:
hobby
和objc
没有下划线_
,是成员变量
nickName
和name
有下划线_
,是属性
继续往下看:
hobby
和objc
没有没有生成setter
和getter
方法,是成员变量
nickName
和name
有生成setter
和getter
方法,是属性
4、由上面我们知道了什么是成员变量和属性,那么实例变量呢?
hobby
不能进行实例化,是成员变量
objc
能进行实例化,是实例变量
,也可以称特殊的成员变量,如:objc = [NSObject alloc]
;
5、我们用一下图表示 成员变量&实例变量&属性
的关系:
6、由上面的分析我们得知了,什么是 属性、成员变量和实例变量
由上面的探索我们了解了什么是 属性
和 成员变量
,我们也可以通过下面的方法来验证一下 属性列表
和 成员变量列表
是不是正确的,如下:
zjObjc_copyIvar_copyProperies
函数:打印变量
和属性
名
1 | void zjObjc_copyIvar_copyProperies(Class pClass){ |
在main.m中调用
1 | int main(int argc, const char * argv[]) { |
查看打印结果:
1 | class_copyIvarList:hobby |
由结果可知:
- 成员变量列表有4个:
hobby、objc、_nickName、_name
。 - 属性列表有2个:
nickName、name
。
成员变量的存储(ivars)
由上面的属性列表分析可得出 property_list
中只有 属性
,没有 成员变量
,那么问题来了, 成员变量存储在哪里
?为什么会有这种情况?
通过 objc4-781最新源码
查看 objc_class
中 bits属性
中存储数据的类 class_rw_t
的结构发现,除了 methods、properties、protocols
方法,还有一个 ro
方法,其返回类型是 class_ro_t
,如下图所示:
点击进去查看 class_ro_t
定义,如下图:
发现其中有一个 ivars属性
,我们可以做如下猜测:是否成员变量就存储在这个 ivar_list_t
类型的 ivars
属性中呢?
下面我们通过 lldb
的调试来验证一下:
由上图lld调试可知:
通过
p *$7
打印成员变量列表,count = 2
,我们知道class_ro_t
中包含两个成员变量sex
和name
通过
bits --> data() -->ro() --> ivars
获取成员变量
列表,除了包括成员变量
,还包括属性
定义的成员变量
通过
@property
定义的属性,也会存储在bits
属性中,通过bits --> data() --> properties() --> list
获取属性列表,其中只存储属性
类方法的存储
我们由前文探索方法列表可知,在 method_list
中 没有类方法
,只有实例方法
,那么问题来了,类方法存储在哪里?为什么会有这种情况?下面我们来仔细分析下
我们在 OC底层原理09:isa走向&继承分析 中,曾提及了 元类
,类对象的 isa
指向就是 元类
,元类是用来 存储类
的 相关信息
的,所以我们猜测:是否 类方法
是否存储在 源类bits方法
中呢?可以通过 lldb
命令来验证我们的猜测。下图是 lldb
命令的调试流程:
由上图lld调试可知:
类的
实例方法
存储在类的bits属性
中,通过bits --> methods() --> list
获取实例方法列表,例如ZJPerson类
的 实例方法sayHello
就存储在ZJPerson类
的bits属性
中,类中的方法列表除了包括实例方法
,还包括系统自动生成的属性
的set方法
、get方法
和cxx_destruct
方法类的
类方法
存储在元类
的bits属性
中,通过元类bits --> methods() --> list
获取类方法列表,例如ZJPerson中的类方法sayBye
就存储在ZJPerson类的元类
(名称也是ZJPerson)的bits属性
中
- Post title:OC底层原理10:类 & 类结构分析
- Post author:张建
- Create time:2020-09-24 21:58:37
- Post link:https://redefine.ohevan.com/2020/09/24/OC底层原理/OC底层原理10:类 & 类结构分析/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.